@@ -107,13 +111,9 @@
{{#if canAct}}
- {{d-button
- title="admin.flags.agree_title"
- class="agree-flag"
- label="admin.flags.agree"
- icon="thumbs-o-up"
- action="showAgreeFlagModal"
- ellipsis=true}}
+ {{admin-agree-flag-dropdown
+ post=flaggedPost
+ removeAfter=(action "removeAfter") }}
{{#if flaggedPost.postHidden}}
{{d-button
@@ -138,12 +138,7 @@
icon="external-link"
label="admin.flags.defer_flag"}}
- {{d-button
- class="btn-danger delete-flag"
- title="admin.flags.delete_title"
- action="showDeleteFlagModal"
- icon="trash-o"
- label="admin.flags.delete"}}
+ {{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}}
{{#unless suspended}}
{{d-button
diff --git a/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs
index ac6935672c..ad9f40585a 100644
--- a/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs
+++ b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs
@@ -1,5 +1,5 @@
{{#each users as |u|}}
- {{#link-to 'adminUser' u class="flagged-topic-user"}}
+ {{#link-to 'adminUser' u.id u.username class="flagged-topic-user"}}
{{avatar u imageSize="small"}}
{{/link-to}}
{{/each}}
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs
index 8c0b4eda1e..0aadec18d8 100644
--- a/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs
+++ b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs
@@ -1,3 +1,3 @@
-{{category-selector categories=selectedCategories blacklist=selectedCategories}}
+{{category-selector categories=selectedCategories}}
{{{unbound setting.description}}}
{{setting-validation-message message=validationMessage}}
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs
index 765a0e20d1..9b3da3178e 100644
--- a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs
+++ b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs
@@ -1,4 +1,4 @@
-{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
+{{combo-box castInteger=true valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
{{preview}}
{{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs
index 3065c09855..d5d3816310 100644
--- a/app/assets/javascripts/admin/templates/customize.hbs
+++ b/app/assets/javascripts/admin/templates/customize.hbs
@@ -1,16 +1,14 @@
-
- {{#admin-nav}}
- {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
- {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
- {{nav-item route='adminSiteText' label='admin.site_text.title'}}
- {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
- {{nav-item route='adminUserFields' label='admin.user_fields.title'}}
- {{nav-item route='adminEmojis' label='admin.emoji.title'}}
- {{nav-item route='adminPermalinks' label='admin.permalink.title'}}
- {{nav-item route='adminEmbedding' label='admin.embedding.title'}}
- {{/admin-nav}}
+{{#admin-nav}}
+ {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
+ {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
+ {{nav-item route='adminSiteText' label='admin.site_text.title'}}
+ {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
+ {{nav-item route='adminUserFields' label='admin.user_fields.title'}}
+ {{nav-item route='adminEmojis' label='admin.emoji.title'}}
+ {{nav-item route='adminPermalinks' label='admin.permalink.title'}}
+ {{nav-item route='adminEmbedding' label='admin.embedding.title'}}
+{{/admin-nav}}
-
- {{outlet}}
-
+
+ {{outlet}}
diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs
index b8c0297d48..6b4dc88584 100644
--- a/app/assets/javascripts/admin/templates/dashboard.hbs
+++ b/app/assets/javascripts/admin/templates/dashboard.hbs
@@ -37,8 +37,8 @@
{{d-icon "shield"}} {{i18n 'admin.dashboard.moderators'}}
{{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}}
- {{d-icon "ban"}} {{i18n 'admin.dashboard.blocked'}}
- {{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}}
+ {{d-icon "ban"}} {{i18n 'admin.dashboard.silenced'}}
+ {{#link-to 'adminUsersList.show' 'silenced'}}{{silenced}}{{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/flags-topics-index.hbs b/app/assets/javascripts/admin/templates/flags-topics-index.hbs
index d9fdf87066..b5a7eca133 100644
--- a/app/assets/javascripts/admin/templates/flags-topics-index.hbs
+++ b/app/assets/javascripts/admin/templates/flags-topics-index.hbs
@@ -37,7 +37,8 @@
ft.id
class="btn d-button no-text btn-small btn-primary show-details"
title=(i18n "admin.flags.show_details")}}
- {{d-icon "search"}}
+ {{d-icon "list"}}
+ {{i18n "admin.flags.details"}}
{{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/logs.hbs b/app/assets/javascripts/admin/templates/logs.hbs
index d72e5a9262..362c376769 100644
--- a/app/assets/javascripts/admin/templates/logs.hbs
+++ b/app/assets/javascripts/admin/templates/logs.hbs
@@ -4,6 +4,7 @@
{{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
{{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
{{nav-item route='adminWatchedWords' label='admin.watched_words.title'}}
+ {{nav-item route='adminLogs.searchLogs' label='admin.logs.search_logs.title'}}
{{#if currentUser.admin}}
{{nav-item path='/logs' label='admin.logs.logster.title'}}
{{/if}}
diff --git a/app/assets/javascripts/admin/templates/logs/search-logs.hbs b/app/assets/javascripts/admin/templates/logs/search-logs.hbs
new file mode 100644
index 0000000000..5b606de9d6
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/logs/search-logs.hbs
@@ -0,0 +1,36 @@
+
+ {{period-chooser period=period}}
+
+
+
+{{#conditional-loading-spinner condition=loading}}
+ {{#if model.length}}
+
+
+
+ {{i18n 'admin.logs.search_logs.term'}}
+ {{i18n 'admin.logs.search_logs.searches'}}
+ {{i18n 'admin.logs.search_logs.click_through'}}
+ {{i18n 'admin.logs.search_logs.most_viewed_topic'}}
+ {{i18n 'admin.logs.search_logs.unique'}}
+
+
+ {{#each model as |item|}}
+
+ {{item.term}}
+ {{item.searches}}
+ {{item.click_through}}
+
+ {{#if item.clicked_topic_id}}
+ {{item.topic_title}}
+ {{/if}}
+
+ {{item.unique}}
+
+ {{/each}}
+
+
+ {{else}}
+ {{i18n 'search.no_results'}}
+ {{/if}}
+{{/conditional-loading-spinner}}
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 c44edc1a3e..5df150c79f 100644
--- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs
+++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs
@@ -1,6 +1,6 @@
{{#if filtersExists}}
-
+
{{i18n 'admin.logs.staff_actions.clear_filters'}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs
deleted file mode 100644
index b9d5a88383..0000000000
--- a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs
+++ /dev/null
@@ -1,35 +0,0 @@
-{{#d-modal-body title="admin.flags.agree_flag_modal_title"}}
- {{#if model.user_deleted}}
- {{d-button
- title="admin.flags.agree_flag_restore_post_title"
- class="confirm-agree-restore"
- action=(action "perform" "restore")
- icon="eye"
- label="admin.flags.agree_flag_restore_post"}}
- {{else}}
- {{#unless model.postHidden}}
- {{d-button
- title="admin.flags.agree_flag_hide_post_title"
- action=(action "perform" "hide")
- class="confirm-agree-hide"
- icon="eye-slash"
- label="admin.flags.agree_flag_hide_post"}}
- {{/unless}}
- {{/if}}
-
- {{d-button
- title="admin.flags.agree_flag_title"
- action=(action "perform" "keep")
- class="confirm-agree-keep"
- icon="thumbs-o-up"
- label="admin.flags.agree_flag"}}
-
- {{#if canDeleteSpammer}}
- {{d-button
- title="admin.flags.delete_spammer_title"
- action="deleteSpammer"
- class="btn-danger delete-spammer"
- icon="exclamation-triangle"
- label="admin.flags.delete_spammer"}}
- {{/if}}
-{{/d-modal-body}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs
deleted file mode 100644
index 571f332dbf..0000000000
--- a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-{{#d-modal-body title="admin.flags.delete_flag_modal_title"}}
- {{d-button
- class="delete-defer"
- title="admin.flags.delete_post_defer_flag_title"
- action="deletePostDeferFlag"
- icon="external-link"
- label="admin.flags.delete_post_defer_flag"}}
-
- {{d-button
- class="delete-agree"
- title="admin.flags.delete_post_agree_flag_title"
- action="deletePostAgreeFlag"
- icon="thumbs-o-up"
- label="admin.flags.delete_post_agree_flag"}}
-
- {{#if canDeleteSpammer}}
- {{d-button
- class="btn-danger delete-spammer"
- title="admin.flags.delete_spammer_title"
- action="deleteSpammer"
- icon="exclamation-triangle"
- label="admin.flags.delete_spammer"}}
- {{/if}}
-{{/d-modal-body}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
new file mode 100644
index 0000000000..0abe77544c
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
@@ -0,0 +1,50 @@
+{{#d-modal-body title="admin.user.silence_modal_title"}}
+ {{#conditional-loading-spinner condition=loadingUser}}
+
+
+
+
+
+
+
+ {{text-field
+ value=reason
+ class="silence-reason"
+ placeholderKey="admin.user.silence_reason_placeholder"}}
+
+
+
+ {{textarea
+ value=message
+ class="silence-message"
+ placeholder=(i18n "admin.user.silence_message_placeholder")}}
+
+
+ {{/conditional-loading-spinner}}
+
+{{/d-modal-body}}
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs b/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs
index f63be2d098..e8e8cd8d2e 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs
@@ -2,5 +2,5 @@
{{model.details}}
{{/d-modal-body}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
index a701cf5045..fc6025a6a6 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
@@ -21,23 +21,22 @@
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
-
- {{text-field
- value=reason
- class="suspend-reason"
- placeholderKey="admin.user.suspend_reason_placeholder"}}
+ {{text-field
+ value=reason
+ class="suspend-reason"
+ placeholderKey="admin.user.suspend_reason_placeholder"}}
+ {{textarea
value=message
class="suspend-message"
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
-
{{else}}
{{i18n "admin.user.cant_suspend"}}
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs
index ec086e3271..9a1325c03f 100644
--- a/app/assets/javascripts/admin/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/templates/user-index.hbs
@@ -17,6 +17,7 @@
{{d-button action="logOut" icon="power-off" label="admin.user.log_out"}}
{{/if}}
{{/if}}
+ {{plugin-outlet name="admin-user-controls-after" args=(hash model=model) tagName="" connectorTagName=""}}
@@ -287,7 +288,7 @@
{{#if model.canLockTrustLevel}}
- {{#if model.trust_level_locked}}
+ {{#if model.manual_locked_trust_level}}
{{d-icon "lock" title="admin.user.trust_level_locked_tip"}}
{{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}}
{{else}}
@@ -346,21 +347,51 @@
{{/if}}
-
- {{i18n 'admin.user.blocked'}}
- {{i18n-yes-no model.blocked}}
+
+ {{i18n 'admin.user.silenced'}}
+
+ {{i18n-yes-no model.silenced}}
+ {{#if model.isSilenced}}
+ {{#unless model.silencedForever}}
+ {{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
+ {{/unless}}
+ {{/if}}
+
- {{#conditional-loading-spinner size="small" condition=model.blockingUser}}
- {{#if model.blocked}}
- {{d-button action="unblock" icon="thumbs-o-up" label="admin.user.unblock"}}
- {{i18n 'admin.user.block_explanation'}}
+ {{#conditional-loading-spinner size="small" condition=model.silencingUser}}
+ {{#if model.silenced}}
+ {{d-button
+ class="btn-danger unsilence-user"
+ action="unsilence"
+ icon="microphone-slash"
+ label="admin.user.unsilence"}}
+ {{i18n 'admin.user.silence_explanation'}}
{{else}}
- {{d-button action="block" icon="ban" label="admin.user.block"}}
- {{i18n 'admin.user.block_explanation'}}
+ {{d-button
+ class="btn-danger silence-user"
+ action=(action "showSilenceModal")
+ icon="microphone-slash"
+ label="admin.user.silence"}}
+ {{i18n 'admin.user.silence_explanation'}}
{{/if}}
{{/conditional-loading-spinner}}
+
+ {{#if model.isSilenced}}
+
+ {{i18n 'admin.user.silenced_by'}}
+
+ {{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
+ {{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
+
+
+ {{i18n 'admin.user.silence_reason'}}:
+ {{model.silence_reason}}
+
+
+ {{/if}}
+
{{#if currentUser.admin}}
@@ -398,7 +429,7 @@
{{i18n 'admin.users.last_emailed'}}
- {{format-date model.last_emailed_at leaveAgo="true"}}
+ {{format-date model.last_emailed_at}}
{{i18n 'last_seen'}}
@@ -443,7 +474,7 @@
{{i18n 'admin.user.time_read'}}
- {{{model.time_read}}}
+ {{{format-duration model.time_read}}}
{{i18n 'user.invited.days_visited'}}
diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs
index cd292cb3e0..0ccaee0251 100644
--- a/app/assets/javascripts/admin/templates/users-list-show.hbs
+++ b/app/assets/javascripts/admin/templates/users-list-show.hbs
@@ -53,17 +53,20 @@
{{avatar user imageSize="small"}}
-
+
{{#link-to 'adminUser' user}}{{unbound user.username}}{{/link-to}}
+ {{#if user.staged}}
+ {{d-icon "envelope-o" title="user.staged" }}
+ {{/if}}
{{unbound user.email}}
- {{{unbound user.last_emailed_age}}}
+ {{{format-duration user.last_emailed_age}}}
- {{{unbound user.last_seen_age}}}
+ {{{format-duration user.last_seen_age}}}
{{number user.topics_entered}}
@@ -72,11 +75,11 @@
{{number user.posts_read_count}}
- {{{unbound user.time_read}}}
+ {{{format-duration user.time_read}}}
- {{{unbound user.created_at_age}}}
+ {{{format-duration user.created_at_age}}}
{{#if showApproval}}
diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs
index d37a9ae267..398732685b 100644
--- a/app/assets/javascripts/admin/templates/users-list.hbs
+++ b/app/assets/javascripts/admin/templates/users-list.hbs
@@ -8,7 +8,7 @@
{{/if}}
{{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
{{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
- {{nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}}
+ {{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}}
{{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
diff --git a/app/assets/javascripts/admin/templates/version-checks.hbs b/app/assets/javascripts/admin/templates/version-checks.hbs
index 4f39265026..a8724b7e5a 100644
--- a/app/assets/javascripts/admin/templates/version-checks.hbs
+++ b/app/assets/javascripts/admin/templates/version-checks.hbs
@@ -24,7 +24,7 @@
{{i18n 'admin.dashboard.no_check_performed'}}
{{else}}
- {{#if versionCheck.staleData}}
+ {{#if versionCheck.stale_data}}
{{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}}
{{#if versionCheck.version_check_pending}}
diff --git a/app/assets/javascripts/admin/templates/web-hooks-show.hbs b/app/assets/javascripts/admin/templates/web-hooks-show.hbs
index 53c0cba146..ef9c478774 100644
--- a/app/assets/javascripts/admin/templates/web-hooks-show.hbs
+++ b/app/assets/javascripts/admin/templates/web-hooks-show.hbs
@@ -48,7 +48,7 @@
- {{category-selector categories=model.categories blacklist=model.categories}}
+ {{category-selector categories=model.categories}}
{{i18n 'admin.web_hooks.categories_filter_instructions'}}
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 280e12954e..6b0d64be84 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -4,7 +4,7 @@
//= require ./ember-addons/ember-computed-decorators
//= require ./ember-addons/fmt
//= require_tree ./discourse-common
-//= require_tree ./select-box-kit
+//= require_tree ./select-kit
//= require ./discourse
//= require ./deprecated
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 0dcf4051cd..84242d03f6 100644
--- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6
+++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6
@@ -42,7 +42,8 @@ export function renderIcon(renderType, id, params) {
let rendererForType = renderer[renderType];
if (rendererForType) {
- let result = rendererForType(REPLACEMENTS[id] || id, params || {});
+ const icon = { id, replacementId: REPLACEMENTS[id] };
+ let result = rendererForType(icon, params || {});
if (result) {
return result;
}
@@ -68,8 +69,9 @@ export function registerIconRenderer(renderer) {
}
// Support for font awesome icons
-function faClasses(id, params) {
- let classNames = `fa fa-${id} d-icon d-icon-${id}`;
+function faClasses(icon, params) {
+ let classNames = `fa fa-${icon.replacementId || icon.id} d-icon d-icon-${icon.id}`;
+
if (params) {
if (params.modifier) { classNames += " fa-" + params.modifier; }
if (params['class']) { classNames += ' ' + params['class']; }
@@ -81,9 +83,9 @@ function faClasses(id, params) {
registerIconRenderer({
name: 'font-awesome',
- string(id, params) {
+ string(icon, params) {
let tagName = params.tagName || 'i';
- let html = `<${tagName} class='${faClasses(id, params)}'`;
+ let html = `<${tagName} class='${faClasses(icon, params)}'`;
if (params.title) { html += ` title='${I18n.t(params.title)}'`; }
if (params.label) { html += " aria-hidden='true'"; }
html += `>${tagName}>`;
@@ -93,11 +95,11 @@ registerIconRenderer({
return html;
},
- node(id, params) {
+ node(icon, params) {
let tagName = params.tagName || 'i';
const properties = {
- className: faClasses(id, params),
+ className: faClasses(icon, params),
attributes: { "aria-hidden": true }
};
diff --git a/app/assets/javascripts/discourse/components/badge-title.js.es6 b/app/assets/javascripts/discourse/components/badge-title.js.es6
index 95da5718bb..322db1f6c2 100644
--- a/app/assets/javascripts/discourse/components/badge-title.js.es6
+++ b/app/assets/javascripts/discourse/components/badge-title.js.es6
@@ -11,9 +11,11 @@ export default Ember.Component.extend(BadgeSelectController, {
save() {
this.setProperties({ saved: false, saving: true });
+ var badge_id = this.get('selectedUserBadgeId') || 0;
+
ajax(this.get('user.path') + "/preferences/badge_title", {
type: "PUT",
- data: { user_badge_id: this.get('selectedUserBadgeId') }
+ data: { user_badge_id: badge_id }
}).then(() => {
this.setProperties({
saved: true,
diff --git a/app/assets/javascripts/discourse/components/category-selector.js.es6 b/app/assets/javascripts/discourse/components/category-selector.js.es6
deleted file mode 100644
index 9cb3eec14a..0000000000
--- a/app/assets/javascripts/discourse/components/category-selector.js.es6
+++ /dev/null
@@ -1,49 +0,0 @@
-import { categoryBadgeHTML } from 'discourse/helpers/category-link';
-import Category from 'discourse/models/category';
-import { on, observes } from 'ember-addons/ember-computed-decorators';
-import { findRawTemplate } from 'discourse/lib/raw-templates';
-
-export default Ember.Component.extend({
- @observes('categories')
- _update() {
- if (this.get('canReceiveUpdates') === 'true')
- this._initializeAutocomplete({updateData: true});
- },
-
- @on('didInsertElement')
- _initializeAutocomplete(opts) {
- const self = this,
- regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
-
- this.$('input').autocomplete({
- items: this.get('categories'),
- single: this.get('single'),
- allowAny: false,
- updateData: (opts && opts.updateData) ? opts.updateData : false,
- dataSource(term) {
- return Category.list().filter(category => {
- const regex = new RegExp(term, 'i');
- return category.get('name').match(regex) &&
- !_.contains(self.get('blacklist') || [], category) &&
- !_.contains(self.get('categories'), category) ;
- });
- },
- onChangeItems(items) {
- const categories = _.map(items, link => {
- const slug = link.match(regexp)[1];
- return Category.findSingleBySlug(slug);
- });
- Em.run.next(() => {
- let existingCategory = _.isArray(self.get('categories')) ? self.get('categories') : [self.get('categories')];
- const result = _.intersection(existingCategory.map(itm => itm.id), categories.map(itm => itm.id));
- if (result.length !== categories.length || existingCategory.length !== categories.length)
- self.set('categories', categories);
- });
- },
- template: findRawTemplate('category-selector-autocomplete'),
- transformComplete(category) {
- return categoryBadgeHTML(category, {allowUncategorized: true});
- }
- });
- }
-});
diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6
index 1d193b5257..8df136d372 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-body.js.es6
@@ -15,6 +15,7 @@ export default Ember.Component.extend(KeyEnterEscape, {
'composer.createdPost:created-post',
'composer.creatingTopic:topic',
'composer.whisper:composing-whisper',
+ 'showPreview:show-preview:hide-preview',
'currentUserPrimaryGroupClass'],
@computed("currentUser.primary_group_name")
@@ -41,19 +42,6 @@ export default Ember.Component.extend(KeyEnterEscape, {
const h = $('#reply-control').height() || 0;
this.movePanels(h + "px");
-
- // Figure out the size of the fields
- const $fields = this.$('.composer-fields');
- const fieldPos = $fields.position();
- if (fieldPos) {
- this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5);
- }
-
- // get the submit panel height
- const submitPos = this.$('.submit-panel').position();
- if (submitPos) {
- this.$('.wmd-controls').css('bottom', h - submitPos.top + 7);
- }
});
},
diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index 8445b9f8a5..4b95913aa4 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -1,5 +1,5 @@
import userSearch from 'discourse/lib/user-search';
-import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
+import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag';
@@ -12,58 +12,36 @@ import { findRawTemplate } from 'discourse/lib/raw-templates';
import { tinyAvatar,
displayErrorForUpload,
getUploadMarkdown,
- validateUploadedFiles } from 'discourse/lib/utilities';
-import { lookupCachedUploadUrl,
- lookupUncachedUploadUrls,
- cacheShortUploadUrl } from 'pretty-text/image-short-url';
+ validateUploadedFiles,
+ formatUsername
+} from 'discourse/lib/utilities';
+import { cacheShortUploadUrl, resolveAllShortUrls } from 'pretty-text/image-short-url';
+
+const REBUILD_SCROLL_MAP_EVENTS = [
+ 'composer:resized',
+ 'composer:typed-reply'
+];
export default Ember.Component.extend({
- classNames: ['wmd-controls'],
- classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls', 'showPreview', 'showPreview::hide-preview'],
+ classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls'],
uploadProgress: 0,
- showPreview: true,
_xhr: null,
+ shouldBuildScrollMap: true,
+ scrollMap: null,
@computed
uploadPlaceholder() {
return `[${I18n.t('uploading')}]() `;
},
- @on('init')
- _setupPreview() {
- const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
- this.set('showPreview', val === 'true');
-
- this.appEvents.on('composer:show-preview', () => {
- this.set('showPreview', true);
- });
-
- this.appEvents.on('composer:hide-preview', () => {
- this.set('showPreview', false);
- });
- },
-
- @computed('site.mobileView', 'showPreview')
- forcePreview(mobileView, showPreview) {
- return mobileView && showPreview;
- },
-
- @computed('showPreview')
- toggleText: function(showPreview) {
- return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
- },
-
- @observes('showPreview')
- showPreviewChanged() {
- this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
- },
-
@computed
markdownOptions() {
return {
previewing: true,
+ formatUsername,
+
lookupAvatarByPostNumber: (postNumber, topicId) => {
const topic = this.get('topic');
if (!topic) { return; }
@@ -75,6 +53,19 @@ export default Ember.Component.extend({
return tinyAvatar(quotedPost.get('avatar_template'));
}
}
+ },
+
+ lookupPrimaryUserGroupByPostNumber: (postNumber, topicId) => {
+ const topic = this.get('topic');
+ if (!topic) { return; }
+
+ const posts = topic.get('postStream.posts');
+ if (posts && topicId === topic.get('id')) {
+ const quotedPost = posts.findBy("post_number", postNumber);
+ if (quotedPost) {
+ return quotedPost.primary_group_name;
+ }
+ }
}
};
},
@@ -83,6 +74,8 @@ export default Ember.Component.extend({
_composerEditorInit() {
const topicId = this.get('topic.id');
const $input = this.$('.d-editor-input');
+ const $preview = this.$('.d-editor-preview');
+
$input.autocomplete({
template: findRawTemplate('user-selector-autocomplete'),
dataSource: term => userSearch({
@@ -94,7 +87,7 @@ export default Ember.Component.extend({
transformComplete: v => v.username || v.name
});
- $input.on('scroll', () => Ember.run.throttle(this, this._syncEditorAndPreviewScroll, 20));
+ this._initInputPreviewSync($input, $preview);
// Focus on the body unless we have a title
if (!this.get('composer.canEditTitle') && !this.capabilities.isIOS) {
@@ -134,29 +127,159 @@ export default Ember.Component.extend({
}
},
- _syncEditorAndPreviewScroll() {
- const $input = this.$('.d-editor-input');
- if (!$input) { return; }
+ _resetShouldBuildScrollMap() {
+ this.set('shouldBuildScrollMap', true);
+ },
- const $preview = this.$('.d-editor-preview');
+ _initInputPreviewSync($input, $preview) {
+ REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
+ this.appEvents.on(event, this, this._resetShouldBuildScrollMap);
+ });
- if ($input.scrollTop() === 0) {
- $preview.scrollTop(0);
- return;
+ Ember.run.scheduleOnce("afterRender", () => {
+ $input.on('touchstart mouseenter', () => {
+ if (!$preview.is(":visible")) return;
+ $preview.off('scroll');
+
+ $input.on('scroll', () => {
+ this._syncScroll(this._syncEditorAndPreviewScroll, $input, $preview);
+ });
+ });
+
+ $preview.on('touchstart mouseenter', () => {
+ $input.off('scroll');
+
+ $preview.on('scroll', () => {
+ this._syncScroll(this._syncPreviewAndEditorScroll, $input, $preview);
+ });
+ });
+ });
+ },
+
+ _syncScroll($callback, $input, $preview) {
+ if (!this.get('scrollMap') || this.get('shouldBuildScrollMap')) {
+ this.set('scrollMap', this._buildScrollMap($input, $preview));
+ this.set('shouldBuildScrollMap', false);
}
- const inputHeight = $input[0].scrollHeight;
- const previewHeight = $preview[0].scrollHeight;
- if (($input.height() + $input.scrollTop() + 100) > inputHeight) {
- // cheat, special case for bottom
- $preview.scrollTop(previewHeight);
- return;
+ Ember.run.throttle(this, $callback, $input, $preview, this.get('scrollMap'), 20);
+ },
+
+ _teardownInputPreviewSync() {
+ [this.$('.d-editor-input'), this.$('.d-editor-preview')].forEach($element => {
+ $element.off("mouseenter touchstart");
+ $element.off("scroll");
+ });
+
+ REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
+ this.appEvents.off(event, this, this._resetShouldBuildScrollMap);
+ });;
+ },
+
+ // Adapted from https://github.com/markdown-it/markdown-it.github.io
+ _buildScrollMap($input, $preview) {
+ let sourceLikeDiv = $('').css({
+ position: 'absolute',
+ height: 'auto',
+ visibility: 'hidden',
+ width: $input[0].clientWidth,
+ 'font-size': $input.css('font-size'),
+ 'font-family': $input.css('font-family'),
+ 'line-height': $input.css('line-height'),
+ 'white-space': $input.css('white-space')
+ }).appendTo('body');
+
+ const linesMap = [];
+ let numberOfLines = 0;
+
+ $input.val().split('\n').forEach(text => {
+ linesMap.push(numberOfLines);
+
+ if (text.length === 0) {
+ numberOfLines++;
+ } else {
+ sourceLikeDiv.text(text);
+
+ let height;
+ let lineHeight;
+ height = parseFloat(sourceLikeDiv.css('height'));
+ lineHeight = parseFloat(sourceLikeDiv.css('line-height'));
+ numberOfLines += Math.round(height / lineHeight);
+ }
+ });
+
+ linesMap.push(numberOfLines);
+ sourceLikeDiv.remove();
+
+ const previewOffsetTop = $preview.offset().top;
+ const offset = $preview.scrollTop() - previewOffsetTop - ($input.offset().top - previewOffsetTop);
+ const nonEmptyList = [];
+ const scrollMap = [];
+ for (let i = 0; i < numberOfLines; i++) { scrollMap.push(-1); };
+
+ nonEmptyList.push(0);
+ scrollMap[0] = 0;
+
+ $preview.find('.preview-sync-line').each((_, element) => {
+ let $element = $(element);
+ let lineNumber = $element.data('line-number');
+ let linesToTop = linesMap[lineNumber];
+ if (linesToTop !== 0) { nonEmptyList.push(linesToTop); }
+ scrollMap[linesToTop] = Math.round($element.offset().top + offset);
+ });
+
+ nonEmptyList.push(numberOfLines);
+ scrollMap[numberOfLines] = $preview[0].scrollHeight;
+
+ let position = 0;
+
+ for (let i = 1; i < numberOfLines; i++) {
+ if (scrollMap[i] !== -1) {
+ position++;
+ continue;
+ }
+
+ let top = nonEmptyList[position];
+ let bottom = nonEmptyList[position + 1];
+
+ scrollMap[i] =
+ ((
+ scrollMap[bottom] * (i - top) +
+ scrollMap[top] * (bottom - i)
+ ) / (bottom - top)).toFixed(2);
+ };
+
+ return scrollMap;
+ },
+
+ _syncEditorAndPreviewScroll($input, $preview, scrollMap) {
+ let scrollTop;
+
+ if (($input.height() + $input.scrollTop() + 100) > $input[0].scrollHeight) {
+ scrollTop = $preview[0].scrollHeight;
+ } else {
+ const lineHeight = parseFloat($input.css('line-height'));
+ const lineNumber = Math.floor($input.scrollTop() / lineHeight);
+ scrollTop = scrollMap[lineNumber];
}
- const scrollPosition = $input.scrollTop();
- const factor = previewHeight / inputHeight;
- const desired = scrollPosition * factor;
- $preview.scrollTop(desired + 50);
+ $preview.stop(true).animate({ scrollTop }, 100, 'linear');
+ },
+
+ _syncPreviewAndEditorScroll($input, $preview, scrollMap) {
+ if (scrollMap.length < 1) return;
+
+ let scrollTop;
+ const previewScrollTop = $preview.scrollTop();
+
+ if (($preview.height() + previewScrollTop + 100) > $preview[0].scrollHeight) {
+ scrollTop = $input[0].scrollHeight;
+ } else {
+ const lineHeight = parseFloat($input.css('line-height'));
+ scrollTop = lineHeight * scrollMap.findIndex(offset => offset > previewScrollTop);
+ }
+
+ $input.stop(true).animate({ scrollTop }, 100, 'linear');
},
_renderUnseenMentions($preview, unseen) {
@@ -198,24 +321,6 @@ export default Ember.Component.extend({
$oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id));
},
- _loadShortUrls($images) {
- const urls = _.map($images, img => $(img).data('orig-src'));
- lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images));
- },
-
- _loadCachedShortUrls($images) {
- $images.each((idx, image) => {
- let $image = $(image);
- let url = lookupCachedUploadUrl($image.data('orig-src'));
- if (url) {
- $image.removeAttr('data-orig-src');
- if (url !== "missing") {
- $image.attr('src', url);
- }
- }
- });
- },
-
_warnMentionedGroups($preview) {
Ember.run.scheduleOnce('afterRender', () => {
var found = this.get('warnedGroupMentions') || [];
@@ -321,6 +426,19 @@ export default Ember.Component.extend({
}
});
+ $element.on("fileuploaddone", (e, data) => {
+ let upload = data.result;
+
+ if (!this._xhr || !this._xhr._userCancelled) {
+ const markdown = getUploadMarkdown(upload);
+ cacheShortUploadUrl(upload.short_url, upload.url);
+ this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown);
+ this._resetUpload(false);
+ } else {
+ this._resetUpload(true);
+ }
+ });
+
$element.on("fileuploadfail", (e, data) => {
this._resetUpload(true);
@@ -328,29 +446,12 @@ export default Ember.Component.extend({
this._xhr = null;
if (!userCancelled) {
- displayErrorForUpload(data);
- }
- });
-
- this.messageBus.subscribe("/uploads/composer", upload => {
- // replace upload placeholder
- if (upload && upload.url) {
- if (!this._xhr || !this._xhr._userCancelled) {
- const markdown = getUploadMarkdown(upload);
- cacheShortUploadUrl(upload.short_url, upload.url);
- this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown);
- this._resetUpload(false);
- } else {
- this._resetUpload(true);
- }
- } else {
- this._resetUpload(true);
- displayErrorForUpload(upload);
+ displayErrorForUpload(data.jqXHR.responseJSON);
}
});
if (this.site.mobileView) {
- this.$(".mobile-file-upload").on("click.uploader", function () {
+ $("#reply-control .mobile-file-upload").on("click.uploader", function () {
// redirect the click on the hidden file input
$("#mobile-uploader").click();
});
@@ -360,29 +461,28 @@ export default Ember.Component.extend({
},
_optionsLocation() {
- // long term we want some smart positioning algorithm in popup-menu
- // the problem is that positioning in a fixed panel is a nightmare
- // cause offsetParent can end up returning a fixed element and then
- // using offset() is not going to work, so you end up needing special logic
- // especially since we allow for negative .top, provided there is room on screen
- const myPos = this.$().position();
- const buttonPos = this.$('.options').position();
+ const composer = $("#reply-control");
+ const composerOffset = composer.offset();
+ const composerPosition = composer.position();
- const popupHeight = $('#reply-control .popup-menu').height();
- const popupWidth = $('#reply-control .popup-menu').width();
+ const buttonBarOffset = $('#reply-control .d-editor-button-bar').offset();
+ const optionsButton = $('#reply-control .d-editor-button-bar .options');
- var top = myPos.top + buttonPos.top - 15;
- var left = myPos.left + buttonPos.left - (popupWidth/2);
+ const popupMenu = $("#reply-control .popup-menu");
+ const popupWidth = popupMenu.outerWidth();
+ const popupHeight = popupMenu.outerHeight();
- const composerPos = $('#reply-control').position();
+ const headerHeight = $(".d-header").outerHeight();
- if (composerPos.top + top - popupHeight < 0) {
- top = top + popupHeight + this.$('.options').height() + 50;
+ let left = optionsButton.offset().left - composerOffset.left;
+ let top = buttonBarOffset.top - composerOffset.top - popupHeight + popupMenu.innerHeight();
+
+ if (top + composerPosition.top - headerHeight - popupHeight < 0) {
+ top += popupHeight + optionsButton.outerHeight();
}
- var replyWidth = $('#reply-control').width();
- if (left + popupWidth > replyWidth) {
- left = replyWidth - popupWidth - 40;
+ if (left + popupWidth > composer.width()) {
+ left -= popupWidth - optionsButton.outerWidth();
}
return { position: "absolute", left, top };
@@ -480,7 +580,7 @@ export default Ember.Component.extend({
@on('willDestroyElement')
_unbindUploadTarget() {
this._validUploads = 0;
- this.$(".mobile-file-upload").off("click.uploader");
+ $("#reply-control .mobile-file-upload").off("click.uploader");
this.messageBus.unsubscribe("/uploads/composer");
const $uploadTarget = this.$();
try { $uploadTarget.fileupload("destroy"); }
@@ -491,14 +591,14 @@ export default Ember.Component.extend({
@on('willDestroyElement')
_composerClosed() {
this.appEvents.trigger('composer:will-close');
- this.appEvents.off('composer:show-preview');
- this.appEvents.off('composer:hide-preview');
Ember.run.next(() => {
$('#main-outlet').css('padding-bottom', 0);
// need to wait a bit for the "slide down" transition of the composer
Ember.run.later(() => this.appEvents.trigger("composer:closed"), 400);
});
+ this._teardownInputPreviewSync();
+
if (this.site.mobileView) {
$(window).off('resize.composer-popup-menu');
}
@@ -528,12 +628,12 @@ export default Ember.Component.extend({
}
},
- showUploadModal(toolbarEvent) {
- this.sendAction('showUploadSelector', toolbarEvent);
+ togglePreview() {
+ this.sendAction('togglePreview');
},
- togglePreview() {
- this.toggleProperty('showPreview');
+ showUploadModal(toolbarEvent) {
+ this.sendAction('showUploadSelector', toolbarEvent);
},
extraButtons(toolbar) {
@@ -605,18 +705,8 @@ export default Ember.Component.extend({
Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450);
}
- // Short upload urls
- let $shortUploadUrls = $('img[data-orig-src]');
-
- if ($shortUploadUrls.length > 0) {
- this._loadCachedShortUrls($shortUploadUrls);
-
- $shortUploadUrls = $('img[data-orig-src]');
- if ($shortUploadUrls.length > 0) {
- // this is carefully batched so we can do an leading debounce (trigger right away)
- Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true);
- }
- }
+ // Short upload urls need resolution
+ resolveAllShortUrls(ajax);
let inline = {};
$('a.inline-onebox-loading', $preview).each(function(index, link) {
@@ -630,6 +720,7 @@ export default Ember.Component.extend({
Ember.run.debounce(this, this._loadInlineOneboxes, inline, 450);
}
+ this._syncScroll(this._syncEditorAndPreviewScroll, this.$('.d-editor-input'), $preview);
this.trigger('previewRefreshed', $preview);
this.sendAction('afterRefresh', $preview);
},
diff --git a/app/assets/javascripts/discourse/components/composer-messages.js.es6 b/app/assets/javascripts/discourse/components/composer-messages.js.es6
index 3f5336c208..6f791315ab 100644
--- a/app/assets/javascripts/discourse/components/composer-messages.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-messages.js.es6
@@ -165,7 +165,6 @@ export default Ember.Component.extend({
if (topicId) { args.topic_id = topicId; }
if (postId) { args.post_id = postId; }
- const queuedForTyping = this.get('queuedForTyping');
composer.store.find('composer-message', args).then(messages => {
if (this.isDestroying || this.isDestroyed) { return; }
@@ -176,6 +175,7 @@ export default Ember.Component.extend({
}
this.set('checkedMessages', true);
+ const queuedForTyping = this.get('queuedForTyping');
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
});
}
diff --git a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 b/app/assets/javascripts/discourse/components/composer-toggles.js.es6
index cd5369638c..205044922e 100644
--- a/app/assets/javascripts/discourse/components/composer-toggles.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-toggles.js.es6
@@ -3,6 +3,14 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
tagName: '',
+ @computed('composeState')
+ title(composeState) {
+ if (composeState === "draft" || composeState === "saving") {
+ return "composer.abandon";
+ }
+ return "composer.collapse";
+ },
+
@computed('composeState')
toggleIcon(composeState) {
if (composeState === "draft" || composeState === "saving") {
diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6
index c248ea5b95..58bddf795a 100644
--- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6
+++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6
@@ -1,7 +1,7 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
- classNameBindings: ['containerClass', 'condition:visible'],
+ classNameBindings: [':loading-container', 'containerClass', 'condition:visible'],
@computed('size')
containerClass(size) {
diff --git a/app/assets/javascripts/discourse/components/cook-text.js.es6 b/app/assets/javascripts/discourse/components/cook-text.js.es6
index 80ed693563..60a96295c8 100644
--- a/app/assets/javascripts/discourse/components/cook-text.js.es6
+++ b/app/assets/javascripts/discourse/components/cook-text.js.es6
@@ -1,4 +1,5 @@
import { cookAsync } from 'discourse/lib/text';
+import { ajax } from 'discourse/lib/ajax';
const CookText = Ember.Component.extend({
tagName: '',
@@ -6,7 +7,16 @@ const CookText = Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
- cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked));
+ cookAsync(this.get('rawText')).then(
+ cooked => {
+ this.set('cooked', cooked);
+ // no choice but to defer this cause
+ // pretty text may only be loaded now
+ Em.run.next(() =>
+ window.requireModule('pretty-text/image-short-url').resolveAllShortUrls(ajax)
+ );
+ }
+ );
}
});
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index 313bfea7a4..ee6725c9cc 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -438,7 +438,9 @@ export default Ember.Component.extend({
}
if (operation !== OP.ADDED &&
- (l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail)) {
+ (l.slice(0, hlen) === hval && tlen === 0 ||
+ (tail.length && l.slice(-tlen) === tail))) {
+
operation = OP.REMOVED;
if (tlen === 0) {
const result = l.slice(hlen);
@@ -500,6 +502,7 @@ export default Ember.Component.extend({
tlen,
opts
);
+
this.set('value', `${pre}${contents}${post}`);
if (lines.length === 1 && tlen > 0) {
this._selectText(sel.start + hlen, sel.value.length);
diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6
new file mode 100644
index 0000000000..ff1e6ef40a
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6
@@ -0,0 +1,25 @@
+import computed from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+ tagName: '',
+
+ @computed('category')
+ showCategoryNotifications(category) {
+ return category && this.currentUser;
+ },
+
+ @computed()
+ categories() {
+ return this.site.get('categoriesList');
+ },
+
+ @computed('category.can_edit')
+ showCategoryEdit: canEdit => canEdit,
+
+ @computed("filterMode", "category", 'noSubcategories')
+ navItems(filterMode, category, noSubcategories) {
+ // we don't want to show the period in the navigation bar since it's in a dropdown
+ if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
+ return Discourse.NavItem.buildList(category, { filterMode, noSubcategories });
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6
index e26c24a21e..906acf2ae8 100644
--- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6
@@ -2,56 +2,69 @@ import DiscourseURL from 'discourse/lib/url';
import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
+import computed from 'ember-addons/ember-computed-decorators';
export default buildCategoryPanel('general', {
foregroundColors: ['FFFFFF', '000000'],
canSelectParentCategory: Em.computed.not('category.isUncategorizedCategory'),
// background colors are available as a pipe-separated string
- backgroundColors: function() {
- const categories = Discourse.Category.list();
+ @computed
+ backgroundColors() {
+ const categories = this.site.get('categoriesList');
return this.siteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat(
categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq();
- }.property(),
+ },
- usedBackgroundColors: function() {
- const categories = Discourse.Category.list();
- const category = this.get('category');
+ @computed
+ noCategoryStyle() {
+ return this.siteSettings.category_style === 'none';
+ },
+
+ @computed('category.id', 'category.color')
+ usedBackgroundColors(categoryId, categoryColor) {
+ const categories = this.site.get('categoriesList');
// If editing a category, don't include its color:
return categories.map(function(c) {
- return (category.get('id') && category.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase();
+ return (categoryId && categoryColor.toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase();
}, this).compact();
- }.property('category.id', 'category.color'),
+ },
- parentCategories: function() {
- return Discourse.Category.list().filter(function (c) {
- return !c.get('parentCategory');
- });
- }.property(),
+ @computed
+ parentCategories() {
+ return this.site.get('categoriesList').filter(c => !c.get('parentCategory'));
+ },
- categoryBadgePreview: function() {
+ @computed(
+ 'category.parent_category_id',
+ 'category.categoryName',
+ 'category.color',
+ 'category.text_color'
+ )
+ categoryBadgePreview(parentCategoryId, name, color, textColor) {
const category = this.get('category');
const c = Category.create({
- name: category.get('categoryName'),
- color: category.get('color'),
- text_color: category.get('text_color'),
- parent_category_id: parseInt(category.get('parent_category_id'),10),
+ name,
+ color,
+ text_color: textColor,
+ parent_category_id: parseInt(parentCategoryId),
read_restricted: category.get('read_restricted')
});
- return categoryBadgeHTML(c, {link: false});
- }.property('category.parent_category_id', 'category.categoryName', 'category.color', 'category.text_color'),
-
+ return categoryBadgeHTML(c, { link: false });
+ },
// We can change the parent if there are no children
- subCategories: function() {
- if (Ember.isEmpty(this.get('category.id'))) { return null; }
- return Category.list().filterBy('parent_category_id', this.get('category.id'));
- }.property('category.id'),
+ @computed('category.id')
+ subCategories(categoryId) {
+ if (Ember.isEmpty(categoryId)) { return null; }
+ return Category.list().filterBy('parent_category_id', categoryId);
+ },
- showDescription: function() {
- return !this.get('category.isUncategorizedCategory') && this.get('category.id');
- }.property('category.isUncategorizedCategory', 'category.id'),
+ @computed('category.isUncategorizedCategory', 'category.id')
+ showDescription(isUncategorizedCategory, categoryId) {
+ return !isUncategorizedCategory && categoryId;
+ },
actions: {
showCategoryTopic() {
diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
index ce75e4fc7e..03efe27a53 100644
--- a/app/assets/javascripts/discourse/components/emoji-picker.js.es6
+++ b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
@@ -474,9 +474,8 @@ export default Ember.Component.extend({
desktopModalePositioning();
} else {
let previewInputOffset = $(".d-editor-input").offset();
- let replyControlOffset = $("#reply-control").offset() || {left: 0};
- let left = previewInputOffset.left - replyControlOffset.left;
- desktopPositioning({left, bottom: $("#reply-control").height() - 48});
+ let left = previewInputOffset.left;
+ desktopPositioning({left, bottom: $("#reply-control").height() - 45});
}
}
}
diff --git a/app/assets/javascripts/discourse/components/expand-post.js.es6 b/app/assets/javascripts/discourse/components/expand-post.js.es6
index 957750e92f..643781dba4 100644
--- a/app/assets/javascripts/discourse/components/expand-post.js.es6
+++ b/app/assets/javascripts/discourse/components/expand-post.js.es6
@@ -2,17 +2,28 @@ import { ajax } from 'discourse/lib/ajax';
export default Ember.Component.extend({
tagName: '',
+ expanded: null,
+ _loading: false,
actions: {
- expandItem() {
+ toggleItem() {
+ if (this._loading) { return false; }
const item = this.get('item');
+
+ if (this.get('expanded')) {
+ this.set('expanded', false);
+ item.set('expandedExcerpt', null);
+ return;
+ }
+
const topicId = item.get('topic_id');
const postNumber = item.get('post_number');
+ this._loading = true;
return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => {
- item.set('truncated', false);
- item.set('excerpt', result.cooked);
- });
+ this.set('expanded', true);
+ item.set('expandedExcerpt', result.cooked);
+ }).finally(() => this._loading = false);
}
}
});
diff --git a/app/assets/javascripts/discourse/components/future-date-input.js.es6 b/app/assets/javascripts/discourse/components/future-date-input.js.es6
index 97b8738787..5c9d37527e 100644
--- a/app/assets/javascripts/discourse/components/future-date-input.js.es6
+++ b/app/assets/javascripts/discourse/components/future-date-input.js.es6
@@ -1,7 +1,7 @@
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import {
FORMAT,
-} from "select-box-kit/components/future-date-input-selector";
+} from "select-kit/components/future-date-input-selector";
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer';
diff --git a/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6
new file mode 100644
index 0000000000..d100e27e25
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: 'li'
+});
diff --git a/app/assets/javascripts/discourse/components/group-navigation.js.es6 b/app/assets/javascripts/discourse/components/group-navigation.js.es6
new file mode 100644
index 0000000000..35f5f238e4
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/group-navigation.js.es6
@@ -0,0 +1,15 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ tagName: '',
+
+ @computed('group')
+ availableTabs(group) {
+ return this.get('tabs').filter(t => {
+ if (t.admin) {
+ return this.currentUser ? this.currentUser.canManageGroup(group) : false;
+ }
+ return true;
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
index 5f9e1266f1..2487a0d50d 100644
--- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
@@ -5,7 +5,7 @@ import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors';
export default Ember.Component.extend({
tagName: 'ul',
classNameBindings: [':nav', ':nav-pills'],
- id: 'navigation-bar',
+ elementId: 'navigation-bar',
init() {
this._super();
diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6
index 9ee2e38905..888eb3c28c 100644
--- a/app/assets/javascripts/discourse/components/navigation-item.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6
@@ -16,7 +16,18 @@ export default Ember.Component.extend(bufferedRender({
buildBuffer(buffer) {
const content = this.get('content');
- buffer.push("");
+
+ let href = content.get('href');
+
+ // Include the category id if the option is present
+ if (content.get('includeCategoryId')) {
+ let categoryId = this.get('category.id');
+ if (categoryId) {
+ href += `?category_id=${categoryId}`;
+ }
+ }
+
+ buffer.push(``);
if (content.get('hasIcon')) {
buffer.push("");
}
diff --git a/app/assets/javascripts/discourse/components/popup-menu.js.es6 b/app/assets/javascripts/discourse/components/popup-menu.js.es6
index 4f8de9b592..64ef71752d 100644
--- a/app/assets/javascripts/discourse/components/popup-menu.js.es6
+++ b/app/assets/javascripts/discourse/components/popup-menu.js.es6
@@ -11,11 +11,8 @@ export default Ember.Component.extend({
this.sendAction('hide');
});
- $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => {
- const $target = $(e.target);
- if ($target.is("button") || this.$().has($target).length === 0) {
- this.sendAction('hide');
- }
+ $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, () => {
+ this.sendAction('hide');
});
},
diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6
index 4d28b05761..5d41b049cc 100644
--- a/app/assets/javascripts/discourse/components/quote-button.js.es6
+++ b/app/assets/javascripts/discourse/components/quote-button.js.es6
@@ -40,9 +40,18 @@ export default Ember.Component.extend({
}
}
- quoteState.selected(postId, selectedText());
+ const _selectedText = selectedText();
+ quoteState.selected(postId, _selectedText);
this.set('visible', quoteState.buffer.length > 0);
+ // avoid hard loops in quote selection unconditionally
+ // this can happen if you triple click text in firefox
+ if (this._prevSelection === _selectedText) {
+ return;
+ }
+
+ this._prevSelection = _selectedText;
+
// on Desktop, shows the button at the beginning of the selection
// on Mobile, shows the button at the end of the selection
const isMobileDevice = this.site.isMobileDevice;
@@ -101,12 +110,14 @@ export default Ember.Component.extend({
const onSelectionChanged = _.debounce(() => this._selectionChanged(), wait);
$(document).on("mousedown.quote-button", e => {
+ this._prevSelection = null;
this._isMouseDown = true;
this._reselected = false;
if ($(e.target).closest('.quote-button, .create, .share, .reply-new').length === 0) {
this._hideButton();
}
}).on("mouseup.quote-button", () => {
+ this._prevSelection = null;
this._isMouseDown = false;
onSelectionChanged();
}).on("selectionchange.quote-button", () => {
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 87e59f60ba..2192c4e3d8 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -4,13 +4,13 @@ import { cloak, uncloak } from 'discourse/widgets/post-stream';
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
import offsetCalculator from 'discourse/lib/offset-calculator';
-function findTopView($posts, viewportTop, min, max) {
+function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
if (max < min) { return min; }
while (max > min) {
const mid = Math.floor((min + max) / 2);
const $post = $($posts[mid]);
- const viewBottom = $post.position().top + $post.height();
+ const viewBottom = ($post.offset().top - postsWrapperTop) + $post.height();
if (viewBottom > viewportTop) {
max = mid-1;
@@ -63,6 +63,10 @@ export default MountWidget.extend({
if (this.isDestroyed || this.isDestroying) { return; }
if (isWorkaroundActive()) { return; }
+ // We use this because watching videos fullscreen in Chrome was super buggy
+ // otherwise. Thanks to arrendek from q23 for the technique.
+ if (document.elementFromPoint(0, 0).tagName.toUpperCase() === "IFRAME") { return; }
+
const $w = $(window);
const windowHeight = window.innerHeight ? window.innerHeight : $w.height();
const slack = Math.round(windowHeight * 5);
@@ -71,9 +75,10 @@ export default MountWidget.extend({
const windowTop = $w.scrollTop();
+ const postsWrapperTop = $('.posts-wrapper').offset().top;
const $posts = this.$('.onscreen-post, .cloaked-post');
const viewportTop = windowTop - slack;
- const topView = findTopView($posts, viewportTop, 0, $posts.length-1);
+ const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1);
let windowBottom = windowTop + windowHeight;
let viewportBottom = windowBottom + slack;
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 f29c6f95a3..a171083523 100644
--- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
+++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
@@ -185,18 +185,18 @@ export default Em.Component.extend({
const userInput = Discourse.Category.findBySlug(subcategories[1], subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
- this.set('searchedTerms.category', [userInput]);
+ this.set('searchedTerms.category', userInput);
} else
if (isNaN(subcategories)) {
const userInput = Discourse.Category.findSingleBySlug(subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
- this.set('searchedTerms.category', [userInput]);
+ this.set('searchedTerms.category', userInput);
} else {
const userInput = Discourse.Category.findById(subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
- this.set('searchedTerms.category', [userInput]);
+ this.set('searchedTerms.category', userInput);
}
} else
this.set('searchedTerms.category', '');
@@ -303,11 +303,11 @@ export default Em.Component.extend({
const slugCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_SLUG) : null;
const idCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_ID) : null;
- if (categoryFilter && categoryFilter[0]) {
- const id = categoryFilter[0].id;
- const slug = categoryFilter[0].slug;
- if (categoryFilter[0].parentCategory) {
- const parentSlug = categoryFilter[0].parentCategory.slug;
+ if (categoryFilter) {
+ const id = categoryFilter.id;
+ const slug = categoryFilter.slug;
+ if (categoryFilter.parentCategory) {
+ const parentSlug = categoryFilter.parentCategory.slug;
if (slugCategoryMatches)
searchTerm = searchTerm.replace(slugCategoryMatches[0], `#${parentSlug}:${slug}`);
else if (idCategoryMatches)
diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
index cfa9340e94..8512ca6794 100644
--- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
@@ -12,6 +12,7 @@ export default MountWidget.extend(Docking, {
buildArgs() {
let attrs = {
topic: this.get('topic'),
+ notificationLevel: this.get('notificationLevel'),
topicTrackingState: this.topicTrackingState,
enteredIndex: this.get('enteredIndex'),
dockAt: 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 c721591563..238b86cdb2 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -6,12 +6,14 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
import DiscourseURL from 'discourse/lib/url';
import User from 'discourse/models/user';
import { userPath } from 'discourse/lib/url';
+import { durationTiny } from 'discourse/lib/formatter';
+import CanCheckEmails from 'discourse/mixins/can-check-emails';
const clickOutsideEventName = "mousedown.outside-user-card";
const clickDataExpand = "click.discourse-user-card";
const clickMention = "click.discourse-user-mention";
-export default Ember.Component.extend(CleansUp, {
+export default Ember.Component.extend(CleansUp, CanCheckEmails, {
elementId: 'user-card',
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'],
allowBackgrounds: setting('allow_profile_backgrounds'),
@@ -29,6 +31,7 @@ export default Ember.Component.extend(CleansUp, {
showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
linkWebsite: Ember.computed.not('user.isBasic'),
hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'),
+ showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'),
visible: false,
user: null,
@@ -87,6 +90,25 @@ export default Ember.Component.extend(CleansUp, {
$this.css('background-image', bg);
},
+ @computed('user.time_read', 'user.recent_time_read')
+ showRecentTimeRead(timeRead, recentTimeRead) {
+ return timeRead !== recentTimeRead && recentTimeRead !== 0;
+ },
+
+ @computed('user.recent_time_read')
+ recentTimeRead(recentTimeReadSeconds) {
+ return durationTiny(recentTimeReadSeconds);
+ },
+
+ @computed('showRecentTimeRead', 'user.time_read', 'recentTimeRead')
+ timeReadTooltip(showRecent, timeRead, recentTimeRead) {
+ if (showRecent) {
+ return I18n.t('time_read_recently_tooltip', {time_read: durationTiny(timeRead), recent_time_read: recentTimeRead});
+ } else {
+ return I18n.t('time_read_tooltip', {time_read: durationTiny(timeRead)});
+ }
+ },
+
_show(username, $target) {
// No user card for anon
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
@@ -271,6 +293,10 @@ export default Ember.Component.extend(CleansUp, {
showUser() {
this.sendAction('showUser', this.get('user'));
this._close();
+ },
+
+ checkEmail(user) {
+ user.checkEmail();
}
}
});
diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6
index fd9bb6ba3b..830a74d708 100644
--- a/app/assets/javascripts/discourse/components/user-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/user-selector.js.es6
@@ -42,7 +42,7 @@ export default TextField.extend({
allowAny: this.get('allowAny'),
updateData: (opts && opts.updateData) ? opts.updateData : false,
- dataSource: function(term) {
+ dataSource(term) {
const termRegex = Discourse.User.currentProp('can_send_private_email_messages') ?
/[^a-zA-Z0-9_\-\.@\+]/ : /[^a-zA-Z0-9_\-\.]/;
@@ -60,7 +60,7 @@ export default TextField.extend({
return results;
},
- transformComplete: function(v) {
+ transformComplete(v) {
if (v.username || v.name) {
if (!v.username) { groups.push(v.name); }
return v.username || v.name;
@@ -72,7 +72,7 @@ export default TextField.extend({
}
},
- onChangeItems: function(items) {
+ onChangeItems(items) {
var hasGroups = false;
items = items.map(function(i) {
if (groups.indexOf(i) > -1) { hasGroups = true; }
@@ -85,7 +85,7 @@ export default TextField.extend({
if (self.get('onChangeCallback')) self.sendAction('onChangeCallback');
},
- reverseTransform: function(i) {
+ reverseTransform(i) {
return { username: i };
}
diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
index ab30fef157..170f28fc30 100644
--- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
+++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
@@ -36,10 +36,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
refreshGravatar() {
this.set("gravatarRefreshDisabled", true);
return ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
- .then(result => this.setProperties({
- gravatar_avatar_template: result.gravatar_avatar_template,
- gravatar_avatar_upload_id: result.gravatar_upload_id,
- }))
+ .then(result => {
+ if (!result.gravatar_avatar_upload_id) {
+ this.set("gravatarFailed", true);
+ } else {
+ this.setProperties({
+ gravatarFailed: false,
+ gravatar_avatar_template: result.gravatar_avatar_template,
+ gravatar_avatar_upload_id: result.gravatar_upload_id,
+ });
+ }
+ })
.finally(() => this.set("gravatarRefreshDisabled", false));
}
}
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index 61f381a239..b9ae01d5f4 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -2,7 +2,7 @@ import DiscourseURL from 'discourse/lib/url';
import Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft';
import Composer from 'discourse/models/composer';
-import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
+import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
import InputValidation from 'discourse/models/input-validation';
import { getOwner } from 'discourse-common/lib/get-owner';
import { escapeExpression } from 'discourse/lib/utilities';
@@ -68,7 +68,28 @@ export default Ember.Controller.extend({
isUploading: false,
topic: null,
linkLookup: null,
+ showPreview: true,
+ forcePreview: Ember.computed.and('site.mobileView', 'showPreview'),
whisperOrUnlistTopic: Ember.computed.or('model.whisper', 'model.unlistTopic'),
+ categories: Ember.computed.alias('site.categoriesList'),
+
+ @on('init')
+ _setupPreview() {
+ const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
+ this.set('showPreview', val === 'true');
+ },
+
+ @computed('showPreview')
+ toggleText: function(showPreview) {
+ return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
+ },
+
+ @observes('showPreview')
+ showPreviewChanged() {
+ if (!this.site.mobileView) {
+ this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
+ }
+ },
@computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames')
focusTarget(replyingToTopic, creatingPM, usernames) {
@@ -205,6 +226,10 @@ export default Ember.Controller.extend({
actions: {
+ togglePreview() {
+ this.toggleProperty('showPreview');
+ },
+
typed() {
this.checkReplyLength();
this.get('model').typing();
@@ -278,20 +303,18 @@ export default Ember.Controller.extend({
// Toggle the reply view
toggle() {
this.closeAutocomplete();
- if (this.get('model.composeState') === Composer.OPEN) {
- if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) {
- this.close();
- } else {
- this.shrink();
- }
- } else {
- this.close();
- }
- return false;
- },
- togglePreview() {
- this.get('model').togglePreview();
+ if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) {
+ this.close();
+ } else {
+ if (this.get('model.composeState') === Composer.OPEN) {
+ this.shrink();
+ } else {
+ this.cancelComposer();
+ }
+ }
+
+ return false;
},
// Import a quote from the post
@@ -367,8 +390,9 @@ export default Ember.Controller.extend({
const body = I18n.t('composer.group_mentioned', {
group: "@" + group.name,
count: group.user_count,
- group_link: Discourse.getURL(`/group/${group.name}/members`)
+ group_link: Discourse.getURL(`/groups/${group.name}/members`)
});
+
this.appEvents.trigger('composer-messages:create', {
extraClass: 'custom-body',
templateName: 'custom-body',
@@ -396,10 +420,6 @@ export default Ember.Controller.extend({
},
- categories: function() {
- return Discourse.Category.list();
- }.property(),
-
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
save(force) {
@@ -654,7 +674,7 @@ export default Ember.Controller.extend({
if (!splitCategory[1]) {
category = this.site.get('categories').findBy('nameLower', splitCategory[0].toLowerCase());
} else {
- const categories = Discourse.Category.list();
+ const categories = this.site.get('categories');
const mainCategory = categories.findBy('nameLower', splitCategory[0].toLowerCase());
category = categories.find(function(item) {
return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id;
@@ -720,7 +740,7 @@ export default Ember.Controller.extend({
},
shrink() {
- if (this.get('model.replyDirty')) {
+ if (this.get('model.replyDirty') || (this.get('model.canEditTitle') && this.get('model.titleDirty'))) {
this.collapse();
} else {
this.close();
diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
index e05c37937b..2af5d3593d 100644
--- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
@@ -65,7 +65,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
saveCategory() {
const self = this,
model = this.get('model'),
- parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10));
+ parentCategory = this.site.get('categories').findBy('id', parseInt(model.get('parent_category_id'), 10));
this.set('saving', true);
model.set('parentCategory', parentCategory);
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 ef661f1928..5fe60501c4 100644
--- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
+++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
@@ -104,11 +104,13 @@ export default Ember.Controller.extend({
cleanTerm(term) {
if (term) {
SortOrders.forEach(order => {
- let matches = term.match(new RegExp(`${order.term}\\b`));
- if (matches) {
- this.set('sortOrder', order.id);
- term = term.replace(new RegExp(`${order.term}\\b`, 'g'), "");
- term = term.trim();
+ if (order.term) {
+ let matches = term.match(new RegExp(`${order.term}\\b`));
+ if (matches) {
+ this.set('sortOrder', order.id);
+ term = term.replace(new RegExp(`${order.term}\\b`, 'g'), "");
+ term = term.trim();
+ }
}
});
}
@@ -159,9 +161,9 @@ export default Ember.Controller.extend({
return this.currentUser && this.currentUser.staff && hasResults;
},
- @computed('expanded')
- canCreateTopic(expanded) {
- return this.currentUser && !this.site.mobileView && !expanded;
+ @computed('expanded', 'model.grouped_search_result.can_create_topic')
+ canCreateTopic(expanded, userCanCreateTopic) {
+ return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded;
},
@computed('expanded')
diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
index c2434c83c6..4047450a1c 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
@@ -1,12 +1,17 @@
+import { observes } from 'ember-addons/ember-computed-decorators';
import { fmt } from 'discourse/lib/computed';
export default Ember.Controller.extend({
group: Ember.inject.controller(),
+ groupActivity: Ember.inject.controller(),
+ application: Ember.inject.controller(),
+ canLoadMore: true,
loading: false,
emptyText: fmt('type', 'groups.empty.%@'),
actions: {
loadMore() {
+ if (!this.get('canLoadMore')) { return; }
if (this.get('loading')) { return; }
this.set('loading', true);
const posts = this.get('model');
@@ -14,12 +19,23 @@ export default Ember.Controller.extend({
const beforePostId = posts[posts.length-1].get('id');
const group = this.get('group.model');
- const opts = { beforePostId, type: this.get('type') };
+ let categoryId = this.get('groupActivity.category_id');
+ const opts = { beforePostId, type: this.get('type'), categoryId };
+
group.findPosts(opts).then(newPosts => {
posts.addObjects(newPosts);
+ if(newPosts.length === 0) {
+ this.set('canLoadMore', false);
+ }
+ }).finally(() => {
this.set('loading', false);
});
}
}
+ },
+
+ @observes('canLoadMore')
+ _showFooter() {
+ this.set("application.showFooter", !this.get("canLoadMore"));
}
});
diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
index 955822ffcf..de4bda4dcd 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
@@ -2,6 +2,7 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
application: Ember.inject.controller(),
+ queryParams: ['category_id'],
@computed('model.is_group_user')
showGroupMessages(isGroupUser) {
diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6
index 4734ac8db4..98e4687f89 100644
--- a/app/assets/javascripts/discourse/controllers/group.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group.js.es6
@@ -1,14 +1,11 @@
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
-var Tab = Em.Object.extend({
- @computed('name')
- location(name) {
- return 'group.' + name;
- },
-
- @computed('name', 'i18nKey')
- message(name, i18nKey) {
- return I18n.t(`groups.${i18nKey || name}`);
+const Tab = Ember.Object.extend({
+ init() {
+ this._super();
+ let name = this.get('name');
+ this.set('route', this.get('route') || `group.` + name);
+ this.set('message', I18n.t(`groups.${this.get('i18nKey') || name}`));
}
});
@@ -18,13 +15,13 @@ export default Ember.Controller.extend({
showing: 'members',
tabs: [
- Tab.create({ name: 'members', 'location': 'group.index', icon: 'users' }),
+ Tab.create({ name: 'members', route: 'group.index', icon: 'users' }),
Tab.create({ name: 'activity' }),
Tab.create({
- name: 'edit', i18nKey: 'edit.title', icon: 'pencil', requiresGroupAdmin: true
+ name: 'edit', i18nKey: 'edit.title', icon: 'pencil', admin: true
}),
Tab.create({
- name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', requiresGroupAdmin: true
+ name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', admin: true
})
],
@@ -58,21 +55,6 @@ export default Ember.Controller.extend({
this.get('tabs')[0].set('count', this.get('model.user_count'));
},
- @computed('model.is_group_owner', 'model.automatic')
- getTabs() {
- return this.get('tabs').filter(t => {
- let canSee = true;
-
- if (this.currentUser && t.requiresGroupAdmin) {
- canSee = this.currentUser.canManageGroup(this.get('model'));
- } else if (t.requiresGroupAdmin) {
- canSee = false;
- }
-
- return canSee;
- });
- },
-
actions: {
messageGroup() {
this.send('createNewMessageViaParams', this.get('model.name'));
diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
index 143812960b..0d757e4bce 100644
--- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
@@ -1,12 +1,6 @@
-import computed from "ember-addons/ember-computed-decorators";
import NavigationDefaultController from 'discourse/controllers/navigation/default';
export default NavigationDefaultController.extend({
showingParentCategory: Em.computed.none('category.parentCategory'),
showingSubcategoryList: Em.computed.and('category.show_subcategory_list', 'showingParentCategory'),
-
- @computed("showingSubcategoryList", "category", "noSubcategories")
- navItems(showingSubcategoryList, category, noSubcategories) {
- return Discourse.NavItem.buildList(category, { noSubcategories });
- }
});
diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
index 93ae014e47..1fa8eedcc7 100644
--- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
+++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
@@ -1,19 +1,4 @@
-import computed from "ember-addons/ember-computed-decorators";
-
export default Ember.Controller.extend({
discovery: Ember.inject.controller(),
discoveryTopics: Ember.inject.controller('discovery/topics'),
-
- @computed()
- categories() {
- return Discourse.Category.list();
- },
-
- @computed("filterMode")
- navItems(filterMode) {
- // we don't want to show the period in the navigation bar since it's in a dropdown
- if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
- return Discourse.NavItem.buildList(null, { filterMode });
- }
-
});
diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6
index e7694a2301..91c62436d6 100644
--- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6
+++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6
@@ -7,6 +7,7 @@ import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(PasswordValidation, {
isDeveloper: Ember.computed.alias('model.is_developer'),
+ admin: Ember.computed.alias('model.admin'),
passwordRequired: true,
errorMessage: null,
successMessage: null,
diff --git a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6
index f2c543e6be..c39fb86395 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6
@@ -1,28 +1,9 @@
-import { ajax } from 'discourse/lib/ajax';
import BadgeSelectController from "discourse/mixins/badge-select-controller";
export default Ember.Controller.extend(BadgeSelectController, {
filteredList: function() {
return this.get('model').filterBy('badge.allow_title', true);
- }.property('model'),
+ }.property('model')
- actions: {
- save() {
- this.setProperties({ saved: false, saving: true });
-
- ajax(this.get('user.path') + "/preferences/badge_title", {
- type: "PUT",
- data: { user_badge_id: this.get('selectedUserBadgeId') }
- }).then(() => {
- this.setProperties({
- saved: true,
- saving: false,
- "user.title": this.get('selectedUserBadge.badge.name')
- });
- }, () => {
- bootbox.alert(I18n.t('generic_error'));
- });
- }
- }
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
index 002eaba949..1b20fabe22 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
@@ -1,8 +1,11 @@
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
+import { setDefaultHomepage } from "discourse/lib/utilities";
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import { currentThemeKey, listThemes, previewTheme, setLocalTheme } from 'discourse/lib/theme-selector';
import { popupAjaxError } from 'discourse/lib/ajax-error';
+const USER_HOMES = { 1: "latest", 2: "categories", 3: "unread", 4: "new", 5: "top" };
+
export default Ember.Controller.extend(PreferencesTabController, {
@computed("makeThemeDefault")
@@ -14,6 +17,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
'enable_quoting',
'disable_jump_reply',
'automatically_unpin_topics',
+ 'allow_private_messages',
+ 'homepage_id',
];
if (makeDefault) {
@@ -51,6 +56,19 @@ export default Ember.Controller.extend(PreferencesTabController, {
previewTheme(key);
},
+ homeChanged() {
+ const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
+ const userHome = USER_HOMES[this.get('model.user_option.homepage_id')];
+ setDefaultHomepage(userHome || siteHome);
+ },
+
+ @computed()
+ userSelectableHome() {
+ return _.map(USER_HOMES, (name, num) => {
+ return {name: I18n.t('filters.' + name + '.title'), value: Number(num)};
+ });
+ },
+
actions: {
save() {
this.set('saved', false);
@@ -66,6 +84,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
setLocalTheme(this.get('themeKey'), this.get('model.user_option.theme_key_seq'));
}
+ this.homeChanged();
+
}).catch(popupAjaxError);
}
}
diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6
index 467750926f..4658b09f38 100644
--- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6
@@ -58,6 +58,8 @@ export default Ember.Controller.extend(BulkTopicSelection, {
max_posts: null,
q: null,
+ categories: Ember.computed.alias('site.categoriesList'),
+
queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'],
navItems: function() {
@@ -68,10 +70,6 @@ export default Ember.Controller.extend(BulkTopicSelection, {
return Discourse.SiteSettings.show_filter_by_tag;
}.property('category'),
- categories: function() {
- return Discourse.Category.list();
- }.property(),
-
showAdminControls: function() {
return !this.get('additionalTags') && this.get('canAdminTag') && !this.get('category');
}.property('additionalTags', 'canAdminTag', 'category'),
diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
index 4b2a19d457..997270dc5f 100644
--- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
@@ -115,7 +115,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
showChangeCategory() {
this.send('changeBulkTemplate', 'modal/bulk-change-category');
- this.set('modal.modalClass', 'topic-bulk-actions-modal full');
},
showNotificationLevel() {
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index fe2a3f4fb0..bc79faf6b0 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -12,6 +12,7 @@ import debounce from 'discourse/lib/debounce';
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import QuoteState from 'discourse/lib/quote-state';
import { userPath } from 'discourse/lib/url';
+import { extractLinkMeta } from 'discourse/lib/render-topic-featured-link';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composer: Ember.inject.controller(),
@@ -32,6 +33,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
username_filters: null,
filter: null,
quoteState: null,
+ canRemoveTopicFeaturedLink: Ember.computed.and('canEditTopicFeaturedLink', 'buffered.featured_link'),
updateQueryParams() {
const postStream = this.get('model.postStream');
@@ -99,6 +101,12 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
},
+ @computed('model')
+ featuredLinkDomain(topic) {
+ const meta = extractLinkMeta(topic);
+ return meta.domain;
+ },
+
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');
@@ -123,9 +131,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
const composer = this.get('composer');
const viewOpen = composer.get('model.viewOpen');
+ const quotedText = Quote.build(post, buffer);
+
// If we can't create a post, delegate to reply as new topic
if ((!viewOpen) && (!this.get('model.details.can_create_post'))) {
- this.send('replyAsNewTopic', post);
+ this.send('replyAsNewTopic', post, quotedText);
return;
}
@@ -146,7 +156,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composerOpts.post = composerPost;
}
- const quotedText = Quote.build(post, buffer);
composerOpts.quote = quotedText;
if (composer.get('model.viewOpen')) {
this.appEvents.trigger('composer:insert-block', quotedText);
@@ -615,11 +624,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}
},
- replyAsNewTopic(post) {
+ replyAsNewTopic(post, quotedText) {
const composerController = this.get('composer');
const { quoteState } = this;
- const quotedText = Quote.build(post, quoteState.buffer);
+ quotedText = quotedText || Quote.build(post, quoteState.buffer);
quoteState.clear();
var options;
@@ -694,6 +703,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
convertToPrivateMessage() {
this.get('content').convertTopic("private");
+ },
+
+ removeFeaturedLink() {
+ this.set('buffered.featured_link', null);
}
},
@@ -752,9 +765,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return selectedPostsUsername !== undefined;
},
- categories: function() {
- return Discourse.Category.list();
- }.property(),
+ categories: Ember.computed.alias('site.categoriesList'),
canSelectAll: Em.computed.not('allPostsSelected'),
diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
index 624a8b0ae7..24d4c33480 100644
--- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
@@ -3,11 +3,9 @@ import { exportUserArchive } from 'discourse/lib/export-csv';
export default Ember.Controller.extend({
application: Ember.inject.controller(),
user: Ember.inject.controller(),
-
userActionType: null,
- currentPath: Ember.computed.alias('application.currentPath'),
- viewingSelf: Ember.computed.alias("user.viewingSelf"),
- showBookmarks: Ember.computed.alias("user.showBookmarks"),
+
+ canDownloadPosts: Ember.computed.alias('user.viewingSelf'),
_showFooter: function() {
var showFooter;
@@ -26,11 +24,7 @@ export default Ember.Controller.extend({
I18n.t("user.download_archive.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
- function(confirmed) {
- if (confirmed) {
- exportUserArchive();
- }
- }
+ confirmed => confirmed ? exportUserArchive() : null
);
}
}
diff --git a/app/assets/javascripts/discourse/controllers/user-summary.js.es6 b/app/assets/javascripts/discourse/controllers/user-summary.js.es6
index bb4bec2077..d489f9ba28 100644
--- a/app/assets/javascripts/discourse/controllers/user-summary.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-summary.js.es6
@@ -1,4 +1,5 @@
import computed from 'ember-addons/ember-computed-decorators';
+import { durationTiny } from 'discourse/lib/formatter';
// should be kept in sync with 'UserSummary::MAX_BADGES'
const MAX_BADGES = 6;
@@ -9,4 +10,19 @@ export default Ember.Controller.extend({
@computed("model.badges.length")
moreBadges(badgesLength) { return badgesLength >= MAX_BADGES; },
+
+ @computed('model.time_read')
+ timeRead(timeReadSeconds) {
+ return durationTiny(timeReadSeconds);
+ },
+
+ @computed('model.time_read', 'model.recent_time_read')
+ showRecentTimeRead(timeRead, recentTimeRead) {
+ return timeRead !== recentTimeRead && recentTimeRead !== 0;
+ },
+
+ @computed('model.recent_time_read')
+ recentTimeRead(recentTimeReadSeconds) {
+ return recentTimeReadSeconds > 0 ? durationTiny(recentTimeReadSeconds) : null;
+ }
});
diff --git a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
index 5682cd2fbd..e62cf98405 100644
--- a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
+++ b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
@@ -1,4 +1,10 @@
import { htmlHelper } from 'discourse-common/lib/helpers';
import { avatarImg } from 'discourse/lib/utilities';
-export default htmlHelper((avatarTemplate, size) => avatarImg({ size, avatarTemplate }));
+export default htmlHelper((avatarTemplate, size) => {
+ if (Ember.isEmpty(avatarTemplate)) {
+ return "";
+ } else {
+ return avatarImg({ size, avatarTemplate });
+ }
+});
diff --git a/app/assets/javascripts/discourse/helpers/format-age.js.es6 b/app/assets/javascripts/discourse/helpers/format-age.js.es6
index 75119d0c5f..a2a52d3d8c 100644
--- a/app/assets/javascripts/discourse/helpers/format-age.js.es6
+++ b/app/assets/javascripts/discourse/helpers/format-age.js.es6
@@ -1,7 +1,11 @@
-import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
+import { autoUpdatingRelativeAge, durationTiny } from 'discourse/lib/formatter';
import { registerUnbound } from 'discourse-common/lib/helpers';
registerUnbound('format-age', function(dt) {
dt = new Date(dt);
return new Handlebars.SafeString(autoUpdatingRelativeAge(dt));
});
+
+registerUnbound('format-duration', function(seconds) {
+ return new Handlebars.SafeString(durationTiny(seconds));
+});
diff --git a/app/assets/javascripts/discourse/helpers/format-username.js.es6 b/app/assets/javascripts/discourse/helpers/format-username.js.es6
new file mode 100644
index 0000000000..dcb8be1840
--- /dev/null
+++ b/app/assets/javascripts/discourse/helpers/format-username.js.es6
@@ -0,0 +1,4 @@
+import { registerUnbound } from 'discourse-common/lib/helpers';
+import { formatUsername } from 'discourse/lib/utilities';
+
+export default registerUnbound('format-username', formatUsername);
diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
index 75e60da098..71e6e87249 100644
--- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
+++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
@@ -1,5 +1,5 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
-import { avatarImg } from 'discourse/lib/utilities';
+import { avatarImg, formatUsername } from 'discourse/lib/utilities';
function renderAvatar(user, options) {
options = options || {};
@@ -11,6 +11,8 @@ function renderAvatar(user, options) {
if (!username || !avatarTemplate) { return ''; }
+ let formattedUsername = formatUsername(username);
+
let title = options.title;
if (!title && !options.ignoreTitle) {
// first try to get a title
@@ -22,7 +24,7 @@ function renderAvatar(user, options) {
// if a description has been provided
if (description && description.length > 0) {
// preprend the username before the description
- title = username + " - " + description;
+ title = formattedUsername + " - " + description;
}
}
}
@@ -30,7 +32,7 @@ function renderAvatar(user, options) {
return avatarImg({
size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses,
- title: title || username,
+ title: title || formattedUsername,
avatarTemplate: avatarTemplate
});
} else {
diff --git a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6
deleted file mode 100644
index 3d8c2f7fd2..0000000000
--- a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6
+++ /dev/null
@@ -1,16 +0,0 @@
-// Android Chrome App Banner requires at least **one** service worker to be instantiate and https.
-// After Discourse starts to use service workers for other stuff (like mobile notification, offline mode, or ember)
-// we can ditch this.
-
-export default {
- name: 'android-app-banner-service-worker',
-
- initialize(container) {
- const caps = container.lookup('capabilities:main');
- const isSecure = document.location.protocol === 'https:';
-
- if (isSecure && caps.isAndroid && 'serviceWorker' in navigator) {
- navigator.serviceWorker.register(Discourse.BaseUri + '/service-worker.js', {scope: './'});
- }
- }
-};
diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
new file mode 100644
index 0000000000..7c16106dda
--- /dev/null
+++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
@@ -0,0 +1,12 @@
+export default {
+ name: 'register-service-worker',
+
+ initialize() {
+ const isSecure = (document.location.protocol === 'https:') ||
+ (location.hostname === "localhost");
+
+ if (isSecure && ('serviceWorker' in navigator)) {
+ navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`);
+ }
+ }
+};
diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6
index b478ddfa43..b26b270b2c 100644
--- a/app/assets/javascripts/discourse/lib/formatter.js.es6
+++ b/app/assets/javascripts/discourse/lib/formatter.js.es6
@@ -129,6 +129,58 @@ function wrapAgo(dateStr) {
return I18n.t("dates.wrap_ago", { date: dateStr });
}
+export function durationTiny(distance, ageOpts) {
+ if (typeof(distance) !== 'number') { return '—'; }
+
+ const dividedDistance = Math.round(distance / 60.0);
+ const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance;
+
+ const t = function(key, opts) {
+ const result = I18n.t("dates.tiny." + key, opts);
+ return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
+ };
+
+ let formatted;
+
+ switch(true) {
+ case(distance <= 59):
+ formatted = t("less_than_x_minutes", {count: 1});
+ break;
+ case(distanceInMinutes >= 0 && distanceInMinutes <= 44):
+ formatted = t("x_minutes", {count: distanceInMinutes});
+ break;
+ case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
+ formatted = t("about_x_hours", {count: 1});
+ break;
+ case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
+ formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
+ break;
+ case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
+ formatted = t("x_days", {count: 1});
+ break;
+ case(distanceInMinutes >= 2520 && distanceInMinutes <= 129599):
+ formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
+ break;
+ case(distanceInMinutes >= 129600 && distanceInMinutes <= 525599):
+ formatted = t("x_months", {count: Math.round(distanceInMinutes / 43200.0)});
+ break;
+ default:
+ const numYears = distanceInMinutes / 525600.0;
+ const remainder = numYears % 1;
+ if (remainder < 0.25) {
+ formatted = t("about_x_years", {count: parseInt(numYears)});
+ } else if (remainder < 0.75) {
+ formatted = t("over_x_years", {count: parseInt(numYears)});
+ } else {
+ formatted = t("almost_x_years", {count: parseInt(numYears) + 1});
+ }
+
+ break;
+ }
+
+ return formatted;
+}
+
function relativeAgeTiny(date, ageOpts) {
const format = "tiny";
const distance = Math.round((new Date() - date) / 1000);
diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
index d8cd970299..688207c5b9 100644
--- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
@@ -1,5 +1,6 @@
import { ajax } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
+import { formatUsername } from 'discourse/lib/utilities';
function replaceSpan($e, username, opts) {
let extra = "";
@@ -16,7 +17,7 @@ function replaceSpan($e, username, opts) {
extra = `data-name='${username}'`;
extraClass = "cannot-see";
}
- $e.replaceWith(`@${username}`);
+ $e.replaceWith(`@${formatUsername(username)}`);
}
}
diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
index 38772203ae..68cc7eb8e6 100644
--- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
@@ -20,10 +20,11 @@ import { addPostTransformCallback } from 'discourse/widgets/post-stream';
import { attachAdditionalPanel } from 'discourse/widgets/header';
import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-library';
import { addNavItem } from 'discourse/models/nav-item';
-
+import { replaceFormatter } from 'discourse/lib/utilities';
+import { modifySelectKit } from "select-kit/mixins/plugin-api";
// If you add any methods to the API ensure you bump up this number
-const PLUGIN_API_VERSION = '0.8.11';
+const PLUGIN_API_VERSION = '0.8.13';
class PluginApi {
constructor(version, container) {
@@ -570,6 +571,40 @@ class PluginApi {
addNavItem(item);
}
}
+
+
+ /**
+ *
+ * Registers a function that will format a username when displayed. This will not
+ * be applied when the username is used as an `id` or in URL strings.
+ *
+ * Example:
+ *
+ * ```
+ * // display usernames in UPPER CASE
+ * api.formatUsername(username => username.toUpperCase());
+ *
+ * ```
+ *
+ **/
+ formatUsername(fn) {
+ replaceFormatter(fn);
+ }
+
+ /**
+ *
+ * Access SelectKit plugin api
+ *
+ * Example:
+ *
+ * modifySelectKit("topic-footer-mobile-dropdown").appendContent(() => [{
+ * name: "discourse",
+ * id: 1
+ * }])
+ */
+ modifySelectKit(pluginApiKey) {
+ return modifySelectKit(pluginApiKey);
+ }
}
let _pluginv01;
diff --git a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
index 3b747284fd..86b30b60bf 100644
--- a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
+++ b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
@@ -1,4 +1,3 @@
-import { extractDomainFromUrl } from 'discourse/lib/utilities';
import { h } from 'virtual-dom';
const _decorators = [];
@@ -7,24 +6,23 @@ export function addFeaturedLinkMetaDecorator(decorator) {
_decorators.push(decorator);
}
-function extractLinkMeta(topic) {
- const href = topic.featured_link,
- target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : '';
+export function extractLinkMeta(topic) {
+ const href = topic.get('featured_link');
+ const target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : '';
if (!href) { return; }
- let domain = extractDomainFromUrl(href);
- if (!domain) { return; }
+ const meta = {
+ target: target,
+ href,
+ domain: topic.get('featured_link_root_domain'),
+ rel: 'nofollow'
+ };
- // www appears frequently, so we truncate it
- if (domain && domain.substr(0, 4) === 'www.') {
- domain = domain.substring(4);
- }
-
- const meta = { target, href, domain, rel: 'nofollow' };
if (_decorators.length) {
_decorators.forEach(cb => cb(meta));
}
+
return meta;
}
@@ -45,4 +43,3 @@ export function topicFeaturedLinkNode(topic) {
}, meta.domain);
}
}
-
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index 42777ed4f1..7cb249b15e 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -1,10 +1,4 @@
-export function isAppleDevice() {
- // IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
- // This will apply hack on all iDevices
- return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
- navigator.userAgent.match(/Safari/g) &&
- !navigator.userAgent.match(/Trident/g);
-}
+import { isAppleDevice } from 'discourse/lib/utilities';
// we can't tell what the actual visible window height is
// because we cannot account for the height of the mobile keyboard
diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6
index 10e7695b57..179a700da5 100644
--- a/app/assets/javascripts/discourse/lib/show-modal.js.es6
+++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6
@@ -1,7 +1,37 @@
+import { isAppleDevice } from 'discourse/lib/utilities';
+
export default function(name, opts) {
opts = opts || {};
const container = Discourse.__container__;
+
+ // iOS 11 -> 11.1 have broken INPUTs on position fixed
+ // if for any reason there is a body higher than 100% behind them.
+ // What happens is that when INPUTs gets focus they shift the body
+ // which ends up moving the cursor to an invisible spot
+ // this makes the login experience on iOS painful, user thinks it is broken.
+ //
+ // Also, very little value in showing main outlet and header on iOS
+ // anyway, so just hide it.
+ if (isAppleDevice()) {
+ let pos = $(window).scrollTop();
+ $(window)
+ .off('show.bs.modal.ios-hacks')
+ .on('show.bs.modal.ios-hacks', () => {
+ $('#main-outlet, header').hide();
+ });
+
+ $(window)
+ .off('hide.bs.modal.ios-hacks')
+ .on('hide.bs.modal.ios-hacks', () => {
+ $('#main-outlet, header').show();
+ $(window).scrollTop(pos);
+
+ $(window).off('hide.bs.modal.ios-hacks');
+ $(window).off('show.bs.modal.ios-hacks');
+ });
+ }
+
// We use the container here because modals are like singletons
// in Discourse. Only one can be shown with a particular state.
const route = container.lookup('route:application');
diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6
index 874f519010..09a7592828 100644
--- a/app/assets/javascripts/discourse/lib/text.js.es6
+++ b/app/assets/javascripts/discourse/lib/text.js.es6
@@ -3,6 +3,7 @@ import { performEmojiUnescape, buildEmojiUrl } from 'pretty-text/emoji';
import WhiteLister from 'pretty-text/white-lister';
import { sanitize as textSanitize } from 'pretty-text/sanitizer';
import loadScript from 'discourse/lib/load-script';
+import { formatUsername } from 'discourse/lib/utilities';
function getOpts(opts) {
const siteSettings = Discourse.__container__.lookup('site-settings:main'),
@@ -12,7 +13,8 @@ function getOpts(opts) {
getURL: Discourse.getURLWithCDN,
currentUser: Discourse.__container__.lookup('current-user:main'),
censoredWords: site.censored_words,
- siteSettings
+ siteSettings,
+ formatUsername
}, opts);
return buildOptions(opts);
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index 1f989f297e..2fe1b79a8b 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -193,10 +193,10 @@ const DiscourseURL = Ember.Object.extend({
path = path.replace(/(https?\:)?\/\/[^\/]+/, '');
// Rewrite /my/* urls
- if (path.indexOf('/my/') === 0) {
+ if (path.indexOf(Discourse.BaseUri + '/my/') === 0) {
const currentUser = Discourse.User.current();
if (currentUser) {
- path = path.replace('/my/', userPath(currentUser.get('username_lower') + "/"));
+ path = path.replace(Discourse.BaseUri + '/my/', userPath(currentUser.get('username_lower') + "/"));
} else {
document.location.href = "/404";
return;
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index a1e55234e2..1b4cfda349 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -1,5 +1,7 @@
import { escape } from 'pretty-text/sanitizer';
+const homepageSelector = 'meta[name=discourse_current_homepage]';
+
export function translateSize(size) {
switch (size) {
case 'tiny': return 20;
@@ -21,6 +23,17 @@ export function escapeExpression(string) {
return escape(string);
}
+let _usernameFormatDelegate = username => username;
+
+export function formatUsername(username) {
+ return _usernameFormatDelegate(username || '');
+}
+
+export function replaceFormatter(fn) {
+ _usernameFormatDelegate = fn;
+}
+
+
export function avatarUrl(template, size) {
if (!template) { return ""; }
const rawSize = getRawSize(translateSize(size));
@@ -272,6 +285,21 @@ function uploadTypeFromFileName(fileName) {
return isAnImage(fileName) ? 'image' : 'attachment';
}
+function isGUID(value) {
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
+}
+
+function imageNameFromFileName(fileName) {
+ const split = fileName.split('.');
+ const name = split[split.length-2];
+
+ if (exports.isAppleDevice() && isGUID(name)) {
+ return I18n.t('upload_selector.default_image_alt_text');
+ }
+
+ return name;
+}
+
export function allowsImages() {
return authorizesAllExtensions() || IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions());
}
@@ -296,8 +324,7 @@ export function uploadLocation(url) {
export function getUploadMarkdown(upload) {
if (isAnImage(upload.original_filename)) {
- const split = upload.original_filename.split('.');
- const name = split[split.length-2];
+ const name = imageNameFromFileName(upload.original_filename);
return ``;
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) {
return uploadLocation(upload.url);
@@ -338,8 +365,22 @@ export function displayErrorForUpload(data) {
}
export function defaultHomepage() {
- // the homepage is the first item of the 'top_menu' site setting
- return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
+ let homepage = null;
+ let elem = _.first($(homepageSelector));
+ if (elem) {
+ homepage = elem.content;
+ }
+ if (!homepage) {
+ homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
+ }
+ return homepage;
+}
+
+export function setDefaultHomepage(homepage) {
+ let elem = _.first($(homepageSelector));
+ if (elem) {
+ elem.content = homepage;
+ }
}
export function determinePostReplaceSelection({ selection, needle, replacement }) {
@@ -372,5 +413,13 @@ export function determinePostReplaceSelection({ selection, needle, replacement }
}
}
+export function isAppleDevice() {
+ // IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
+ // This will apply hack on all iDevices
+ return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
+ navigator.userAgent.match(/Safari/g) &&
+ !navigator.userAgent.match(/Trident/g);
+}
+
// This prevents a mini racer crash
export default {};
diff --git a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 b/app/assets/javascripts/discourse/mixins/password-validation.js.es6
index 7e9caf62dc..b65272e03f 100644
--- a/app/assets/javascripts/discourse/mixins/password-validation.js.es6
+++ b/app/assets/javascripts/discourse/mixins/password-validation.js.es6
@@ -16,13 +16,13 @@ export default Ember.Mixin.create({
return I18n.t('user.password.instructions', {count: this.get('passwordMinLength')});
},
- @computed('isDeveloper')
- passwordMinLength() {
- return this.get('isDeveloper') ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length;
+ @computed('isDeveloper', 'admin')
+ passwordMinLength(isDeveloper, admin) {
+ return (isDeveloper || admin) ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length;
},
- @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'isDeveloper')
- passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, isDeveloper) {
+ @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'passwordMinLength')
+ passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, passwordMinLength) {
if (!passwordRequired) {
return InputValidation.create({ ok: true });
}
@@ -40,8 +40,7 @@ export default Ember.Mixin.create({
}
// If too short
- const passwordLength = isDeveloper ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length;
- if (password.length < passwordLength) {
+ if (password.length < passwordMinLength) {
return InputValidation.create({
failed: true,
reason: I18n.t('user.password.too_short')
diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6
index ec76aa1e66..1dc55eeb85 100644
--- a/app/assets/javascripts/discourse/mixins/upload.js.es6
+++ b/app/assets/javascripts/discourse/mixins/upload.js.es6
@@ -18,12 +18,9 @@ export default Em.Mixin.create({
uploadUrl = Discourse.getURL(this.getWithDefault("uploadUrl", "/uploads")),
reset = () => this.setProperties({ uploading: false, uploadProgress: 0});
- this.messageBus.subscribe("/uploads/" + this.get("type"), upload => {
- if (upload && upload.url) {
- this.uploadDone(upload);
- } else {
- displayErrorForUpload(upload);
- }
+ $upload.on("fileuploaddone", (e, data) => {
+ let upload = data.result;
+ this.uploadDone(upload);
reset();
});
@@ -59,7 +56,7 @@ export default Em.Mixin.create({
});
$upload.on("fileuploadfail", (e, data) => {
- displayErrorForUpload(data);
+ displayErrorForUpload(data.jqXHR.responseJSON);
reset();
});
}.on("didInsertElement"),
diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6
index 16304f453e..52c3eb443c 100644
--- a/app/assets/javascripts/discourse/models/category.js.es6
+++ b/app/assets/javascripts/discourse/models/category.js.es6
@@ -90,6 +90,7 @@ const Category = RestModel.extend({
position: this.get('position'),
email_in: this.get('email_in'),
email_in_allow_strangers: this.get('email_in_allow_strangers'),
+ mailinglist_mirror: this.get('mailinglist_mirror'),
parent_category_id: this.get('parent_category_id'),
uploaded_logo_id: this.get('uploaded_logo.id'),
uploaded_background_id: this.get('uploaded_background.id'),
@@ -205,9 +206,7 @@ Category.reopenClass({
},
list() {
- return Discourse.SiteSettings.fixed_category_positions ?
- Discourse.Site.currentProp('categories') :
- Discourse.Site.currentProp('sortedCategories');
+ return Discourse.Site.currentProp('categoriesList');
},
listByActivity() {
diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 48e8e26196..37ac8b9eba 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -46,6 +46,12 @@ const CLOSED = 'closed',
featuredLink: 'topic.featured_link'
};
+const _saveLabels = {};
+_saveLabels[EDIT] = 'composer.save_edit';
+_saveLabels[REPLY] = 'composer.reply';
+_saveLabels[CREATE_TOPIC] = 'composer.create_topic';
+_saveLabels[PRIVATE_MESSAGE] = 'composer.create_pm';
+
const Composer = RestModel.extend({
_categoryId: null,
unlistTopic: false,
@@ -85,7 +91,7 @@ const Composer = RestModel.extend({
@computed("privateMessage", "archetype.hasOptions")
showCategoryChooser(isPrivateMessage, hasOptions) {
- const manyCategories = Discourse.Category.list().length > 1;
+ const manyCategories = this.site.get('categories').length > 1;
return !isPrivateMessage && (hasOptions || manyCategories);
},
@@ -250,14 +256,9 @@ const Composer = RestModel.extend({
}
},
- @computed('action')
- saveLabel(action) {
- switch (action) {
- case EDIT: return 'composer.save_edit';
- case REPLY: return 'composer.reply';
- case CREATE_TOPIC: return 'composer.create_topic';
- case PRIVATE_MESSAGE: return 'composer.create_pm';
- }
+ @computed('action', 'whisper')
+ saveLabel(action, whisper) {
+ return whisper ? 'composer.create_whisper' : _saveLabels[action];
},
hasMetaData: function() {
@@ -274,6 +275,16 @@ const Composer = RestModel.extend({
return this.get('reply') !== this.get('originalText');
}.property('reply', 'originalText'),
+ /**
+ Did the user make changes to the topic title?
+
+ @property titleDirty
+ **/
+ @computed('title', 'originalTitle')
+ titleDirty(title, originalTitle) {
+ return title !== originalTitle;
+ },
+
/**
Number of missing characters in the title until valid.
@@ -481,7 +492,7 @@ const Composer = RestModel.extend({
this.set('categoryId', opts.categoryId || this.get('topic.category.id'));
if (!this.get('categoryId') && this.get('creatingTopic')) {
- const categories = Discourse.Category.list();
+ const categories = this.site.get('categories');
if (categories.length === 1) {
this.set('categoryId', categories[0].get('id'));
}
@@ -518,6 +529,9 @@ const Composer = RestModel.extend({
}
if (opts.title) { this.set('title', opts.title); }
this.set('originalText', opts.draft ? '' : this.get('reply'));
+ if (this.get('editingFirstPost')) {
+ this.set('originalTitle', this.get('title'));
+ }
return false;
},
@@ -740,10 +754,18 @@ const Composer = RestModel.extend({
saveDraft() {
// Do not save when drafts are disabled
if (this.get('disableDrafts')) return;
- // Do not save when there is no reply
- if (!this.get('reply')) return;
- // Do not save when the reply's length is too small
- if (this.get('replyLength') < this.siteSettings.min_post_length) return;
+
+ if (this.get('canEditTitle')) {
+ // Save title and/or post body
+ if (!this.get('title') && !this.get('reply')) return;
+ if (this.get('title') && this.get('titleLengthValid') &&
+ this.get('reply') && this.get('replyLength') < this.siteSettings.min_post_length) return;
+ } else {
+ // Do not save when there is no reply
+ if (!this.get('reply')) return;
+ // Do not save when the reply's length is too small
+ if (this.get('replyLength') < this.siteSettings.min_post_length) return;
+ }
const data = {
reply: this.get('reply'),
diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6
index c7e5eed6b1..c26544e7c5 100644
--- a/app/assets/javascripts/discourse/models/group.js.es6
+++ b/app/assets/javascripts/discourse/models/group.js.es6
@@ -203,12 +203,13 @@ const Group = RestModel.extend({
findPosts(opts) {
opts = opts || {};
- const type = opts['type'] || 'posts';
+ const type = opts.type || 'posts';
var data = {};
if (opts.beforePostId) { data.before_post_id = opts.beforePostId; }
+ if (opts.categoryId) { data.category_id = parseInt(opts.categoryId); }
- return ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => {
+ return ajax(`/groups/${this.get('name')}/${type}.json`, { data }).then(posts => {
return posts.map(p => {
p.user = User.create(p.user);
p.topic = Topic.create(p.topic);
diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6
index f71842cd6e..7616644968 100644
--- a/app/assets/javascripts/discourse/models/nav-item.js.es6
+++ b/app/assets/javascripts/discourse/models/nav-item.js.es6
@@ -111,6 +111,7 @@ NavItem.reopenClass({
opts = opts || {};
if (anonymous && !Discourse.Site.currentProp('anonymous_top_menu_items').includes(testName)) return null;
+
if (!Discourse.Category.list() && testName === "categories") return null;
if (!Discourse.Site.currentProp('top_menu_items').includes(testName)) return null;
diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6
index ace9f7176c..073f6b43f9 100644
--- a/app/assets/javascripts/discourse/models/site.js.es6
+++ b/app/assets/javascripts/discourse/models/site.js.es6
@@ -54,6 +54,14 @@ const Site = RestModel.extend({
return result;
},
+ // Returns it in the correct order, by setting
+ @computed
+ categoriesList() {
+ return this.siteSettings.fixed_category_positions ?
+ this.get('categories') :
+ this.get('sortedCategories');
+ },
+
postActionTypeById(id) {
return this.get("postActionByIdLookup.action" + id);
},
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index 3727290485..df6fd563cc 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -135,7 +135,7 @@ const Topic = RestModel.extend({
const categoryName = this.get('categoryName');
let category;
if (categoryName) {
- category = Discourse.Category.list().findBy('name', categoryName);
+ category = this.site.get('categories').findBy('name', categoryName);
}
this.set('category', category);
}.observes('categoryName'),
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 17e7f9bfc1..9925d4798a 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -16,6 +16,8 @@ import PreloadStore from 'preload-store';
import { defaultHomepage } from 'discourse/lib/utilities';
import { userPath } from 'discourse/lib/url';
+const isForever = dt => moment().diff(dt, 'years') < -500;
+
const User = RestModel.extend({
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
@@ -178,14 +180,16 @@ const User = RestModel.extend({
},
@computed("suspended_till")
- suspendedForever(suspendedTill) {
- return moment().diff(suspendedTill, 'years') < -500;
- },
+ suspendedForever: isForever,
+
+ @computed("silenced_till")
+ silencedForever: isForever,
@computed("suspended_till")
- suspendedTillDate(suspendedTill) {
- return longDate(suspendedTill);
- },
+ suspendedTillDate: longDate,
+
+ @computed("silenced_till")
+ silencedTillDate: longDate,
changeUsername(new_username) {
return ajax(userPath(`${this.get('username_lower')}/preferences/username`), {
@@ -249,6 +253,7 @@ const User = RestModel.extend({
'include_tl0_in_digests',
'theme_key',
'allow_private_messages',
+ 'homepage_id',
];
if (fields) {
@@ -461,7 +466,7 @@ const User = RestModel.extend({
"delete": function() {
if (this.get('can_delete_account')) {
- return ajax(userPath(this.get('username')), {
+ return ajax(userPath(this.get('username') + ".json"), {
type: 'DELETE',
data: {context: window.location.pathname}
});
@@ -472,7 +477,7 @@ const User = RestModel.extend({
dismissBanner(bannerKey) {
this.set("dismissed_banner_key", bannerKey);
- ajax(userPath(this.get('username')), {
+ ajax(userPath(this.get('username') + ".json"), {
type: 'PUT',
data: { dismissed_banner_key: bannerKey }
});
diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
index ca60c3c311..453ee0612b 100644
--- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
+++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
@@ -36,14 +36,14 @@ export default {
app.register('topic-tracking-state:main', topicTrackingState, { instantiate: false });
ALL_TARGETS.forEach(t => app.inject(t, 'topicTrackingState', 'topic-tracking-state:main'));
- const site = Discourse.Site.current();
- app.register('site:main', site, { instantiate: false });
- ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main'));
-
const siteSettings = Discourse.SiteSettings;
app.register('site-settings:main', siteSettings, { instantiate: false });
ALL_TARGETS.forEach(t => app.inject(t, 'siteSettings', 'site-settings:main'));
+ const site = Discourse.Site.current();
+ app.register('site:main', site, { instantiate: false });
+ ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main'));
+
app.register('search-service:main', SearchService);
ALL_TARGETS.forEach(t => app.inject(t, 'searchService', 'search-service:main'));
diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6
index 0f8fb98d39..4b29783e56 100644
--- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6
@@ -49,7 +49,6 @@ export default (filterArg, params) => {
category,
filterMode: filterMode,
noSubcategories: params && params.no_subcategories,
- canEditCategory: category.get('can_edit')
});
},
diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
index d254e0e6a2..0ba2194e50 100644
--- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
@@ -12,10 +12,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
this.render("discovery/categories", { outlet: "list-container" });
},
- beforeModel() {
- this.controllerFor("navigation/categories").set("filterMode", "categories");
- },
-
model() {
const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style;
const parentCategory = this.get("model.parentCategory");
@@ -81,7 +77,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
controller.set("model", model);
this.controllerFor("navigation/categories").setProperties({
- canCreateCategory: model.get("can_create_category"),
+ showCategoryAdmin: model.get("can_create_category"),
canCreateTopic: model.get("can_create_topic"),
});
diff --git a/app/assets/javascripts/discourse/routes/group-activity.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-index.js.es6
similarity index 100%
rename from app/assets/javascripts/discourse/routes/group-activity.js.es6
rename to app/assets/javascripts/discourse/routes/group-activity-index.js.es6
diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
index 03cd7ec114..827c8c468b 100644
--- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
@@ -6,12 +6,13 @@ export function buildGroupPage(type) {
return I18n.t(`groups.${type}`);
},
- model() {
- return this.modelFor("group").findPosts({ type });
+ model(params, transition) {
+ let categoryId = Ember.get(transition, 'queryParams.category_id');
+ return this.modelFor("group").findPosts({ type, categoryId });
},
setupController(controller, model) {
- this.controllerFor('group-activity-posts').setProperties({ model, type });
+ this.controllerFor('group-activity-posts').setProperties({ model, type, canLoadMore: true });
this.controllerFor("group").set("showing", type);
},
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
index c4f3a3d913..19d84f9b7e 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
@@ -8,6 +8,14 @@ export default createPMRoute('groups', 'private-messages-groups').extend({
});
},
+ afterModel(model) {
+ const split = model.get("filter").split('/');
+ const groupName = split[split.length-2];
+ const groups = this.modelFor("user").get("groups");
+ const group = _.first(groups.filterBy("name", groupName));
+ this.controllerFor("user-private-messages").set("group", group);
+ },
+
setupController(controller, model) {
this._super.apply(this, arguments);
const split = model.get("filter").split('/');
diff --git a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs b/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs
deleted file mode 100644
index 4856de5179..0000000000
--- a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/assets/javascripts/discourse/templates/components/category-selector.hbs b/app/assets/javascripts/discourse/templates/components/category-selector.hbs
deleted file mode 100644
index a265ef5f12..0000000000
--- a/app/assets/javascripts/discourse/templates/components/category-selector.hbs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs
index 3d3f6fc8eb..3c7f581375 100644
--- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs
+++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs
@@ -11,30 +11,9 @@
validation=validation
loading=composer.loading
forcePreview=forcePreview
- composerEvents=true}}
+ composerEvents=true
+ autofocus=true}}
-
- {{#if site.mobileView}}
+{{#if site.mobileView}}
- {{i18n 'upload'}}
-
- {{#if showPreview}}
- {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}}
- {{/if}}
- {{else}}
- {{{toggleText}}}
- {{/if}}
-
- {{#if isUploading}}
-
- {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}}
- {{uploadProgress}}%
- {{#if isCancellable}}
- {{d-icon "times"}}
- {{/if}}
-
- {{/if}}
-
- {{draftStatus}}
-
-
+{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs
index 09e841bd85..93263ae3cb 100644
--- a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs
+++ b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs
@@ -1,12 +1,14 @@
-{{#if site.mobileView}}
- {{flat-button
- class="toggle-toolbar"
- icon="bars"
- action=toggleToolbar}}
-{{/if}}
+
+ {{#if site.mobileView}}
+ {{flat-button
+ class="toggle-toolbar"
+ icon="bars"
+ action=toggleToolbar}}
+ {{/if}}
-{{flat-button
- class="toggler"
- icon=toggleIcon
- action=toggleComposer
- title='composer.toggler'}}
+ {{flat-button
+ class="toggler"
+ icon=toggleIcon
+ action=toggleComposer
+ title=title}}
+
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
index 961b63c6ef..c928412ebc 100644
--- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
@@ -1,4 +1,5 @@
+
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}
{{i18n "composer.link_dialog_title"}}
@@ -8,19 +9,18 @@
-
-
-
+
+
{{conditional-loading-spinner condition=loading}}
{{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}}
{{popup-input-tip validation=validation}}
@@ -30,7 +30,7 @@
{{{preview}}}
- {{plugin-outlet name="editor-preview"}}
+ {{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs
new file mode 100644
index 0000000000..6f8c71f524
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs
@@ -0,0 +1,26 @@
+{{bread-crumbs categories=categories category=category noSubcategories=noSubcategories}}
+
+{{#if showCategoryAdmin}}
+ {{categories-admin-dropdown
+ create=createCategory
+ reorder=reorderCategories}}
+{{/if}}
+
+{{navigation-bar navItems=navItems filterMode=filterMode category=category}}
+
+{{#if showCategoryNotifications}}
+ {{category-notifications-button value=category.notification_level category=category}}
+{{/if}}
+
+{{create-topic-button
+ canCreateTopic=canCreateTopic
+ action=createTopic
+ disabled=createTopicDisabled}}
+
+{{#if showCategoryEdit}}
+ {{d-button
+ class="btn-default edit-category"
+ action=editCategory
+ icon="wrench"
+ label="category.edit_long"}}
+{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/directory-item.hbs b/app/assets/javascripts/discourse/templates/components/directory-item.hbs
index 4b1d9a940e..e7758227f4 100644
--- a/app/assets/javascripts/discourse/templates/components/directory-item.hbs
+++ b/app/assets/javascripts/discourse/templates/components/directory-item.hbs
@@ -7,5 +7,5 @@
{{number item.posts_read}}
{{number item.days_visited}}
{{#if showTimeRead}}
- {{unbound item.time_read}}
+ {{format-duration item.time_read}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs
index c988d53869..c31406819a 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs
@@ -22,7 +22,9 @@
{{category-chooser
none="category.none"
value=category.parent_category_id
+ excludeCategoryId=category.id
categories=parentCategories
+ allowSubCategories=false
allowUncategorized=false}}
{{/if}}
@@ -37,28 +39,30 @@
{{i18n 'category.no_description'}}
{{/if}}
{{#if category.topic_url}}
-
+
{{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}}
{{/if}}
{{/if}}
-
-
-
- {{{categoryBadgePreview}}}
+ {{#unless noCategoryStyle}}
+
+
+
+ {{{categoryBadgePreview}}}
-
- {{i18n 'category.background_color'}}:
- #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}}
- {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
-
+
+ {{i18n 'category.background_color'}}:
+ #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}}
+ {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
+
-
- {{i18n 'category.foreground_color'}}:
- #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
- {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}}
+
+ {{i18n 'category.foreground_color'}}:
+ #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
+ {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}}
+
-
-
+
+ {{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
index a389e2ceab..fb0a13c213 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
@@ -9,21 +9,30 @@
{{{i18n "category.can"}}}
{{#if editingPermissions}}
- {{d-icon "times-circle"}}
+ {{d-icon "times-circle"}}
{{/if}}
{{/each}}
{{#if editingPermissions}}
- {{combo-box content=category.availableGroups value=selectedGroup}}
- {{combo-box class="permission-selector"
- nameProperty="description"
- content=category.availablePermissions
- value=selectedPermission}}
-
+ {{#if category.availableGroups}}
+ {{combo-box class="available-groups"
+ allowInitialValueMutation=true
+ allowsContentReplacement=true
+ content=category.availableGroups
+ value=selectedGroup}}
+ {{combo-box allowInitialValueMutation=true
+ class="permission-selector"
+ nameProperty="description"
+ content=category.availablePermissions
+ value=selectedPermission}}
+
+ {{/if}}
{{else}}
{{#unless category.is_special}}
-
+
{{/unless}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
index f209d17af2..fa7b2a5653 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
@@ -47,15 +47,19 @@
+
+ {{combo-box valueAttribute="value" content=availableViews value=category.default_view}}
+
+
+ {{combo-box valueAttribute="value" content=availableTopPeriods value=category.default_top_period}}
+
@@ -115,6 +119,13 @@
+
+
+
+
{{plugin-outlet name="category-email-in" args=(hash category=category)}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs
index c7929241b8..f90534931b 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs
@@ -1,6 +1,6 @@
+ {{period-chooser period=period}} +
++ +{{#conditional-loading-spinner condition=loading}} + {{#if model.length}} + +
{{model.details}}
{{/d-modal-body}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
index a701cf5045..fc6025a6a6 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
@@ -21,23 +21,22 @@
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
{{i18n "composer.link_dialog_title"}}
@@ -8,19 +9,18 @@+
{{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}} {{/if}} {{/if}} -