Version bump

This commit is contained in:
Neil Lalonde 2017-04-10 14:32:26 -04:00
commit 6bb2dd0584
299 changed files with 5166 additions and 4950 deletions

View File

@ -42,6 +42,7 @@ before_install:
- git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert
- git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
- git clone --depth=1 https://github.com/discourse/discourse-slack-official.git plugins/discourse-slack-official
- npm i -g eslint babel-eslint
- eslint app/assets/javascripts
- eslint --ext .es6 app/assets/javascripts

View File

@ -1,8 +1,12 @@
import { ajax } from 'discourse/lib/ajax';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import { userPath } from 'discourse/lib/url';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(CanCheckEmails, {
editingUsername: false,
editingName: false,
editingTitle: false,
originalPrimaryGroupId: null,
availableGroups: null,
@ -54,23 +58,58 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
destroy() { return this.get('model').destroy(); },
toggleUsernameEdit() {
this.set('userUsernameValue', this.get('model.username'));
this.toggleProperty('editingUsername');
},
saveUsername() {
const oldUsername = this.get('model.username');
this.set('model.username', this.get('userUsernameValue'));
return ajax(`/users/${oldUsername.toLowerCase()}/preferences/username`, {
data: { new_username: this.get('userUsernameValue') },
type: 'PUT'
}).catch(e => {
this.set('model.username', oldUsername);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingUsername'));
},
toggleNameEdit() {
this.set('userNameValue', this.get('model.name'));
this.toggleProperty('editingName');
},
saveName() {
const oldName = this.get('model.name');
this.set('model.name', this.get('userNameValue'));
return ajax(userPath(`${this.get('model.username').toLowerCase()}.json`), {
data: { name: this.get('userNameValue') },
type: 'PUT'
}).catch(e => {
this.set('model.name', oldName);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingName'));
},
toggleTitleEdit() {
this.set('userTitleValue', this.get('model.title'));
this.toggleProperty('editingTitle');
},
saveTitle() {
const self = this;
const prevTitle = this.get('userTitleValue');
return ajax(`/users/${this.get('model.username').toLowerCase()}.json`, {
this.set('model.title', this.get('userTitleValue'));
return ajax(userPath(`${this.get('model.username').toLowerCase()}.json`), {
data: {title: this.get('userTitleValue')},
type: 'PUT'
}).catch(function(e) {
bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body}));
}).finally(function() {
self.set('model.title', self.get('userTitleValue'));
self.toggleProperty('editingTitle');
});
}).catch(e => {
this.set('model.title', prevTitle);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingTitle'));
},
generateApiKey() {

View File

@ -7,7 +7,7 @@ import { observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
query: null,
queryParams: ['order', 'ascending'],
order: 'seen',
order: null,
ascending: null,
showEmails: false,
refreshing: false,

View File

@ -5,6 +5,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import ApiKey from 'admin/models/api-key';
import Group from 'discourse/models/group';
import TL3Requirements from 'admin/models/tl3-requirements';
import { userPath } from 'discourse/lib/url';
const AdminUser = Discourse.User.extend({
@ -114,11 +115,10 @@ const AdminUser = Discourse.User.extend({
},
revokeAdmin() {
const self = this;
return ajax("/admin/users/" + this.get('id') + "/revoke_admin", {
return ajax(`/admin/users/${this.get('id')}/revoke_admin`, {
type: 'PUT'
}).then(function() {
self.setProperties({
}).then(() => {
this.setProperties({
admin: false,
can_grant_admin: true,
can_revoke_admin: false
@ -127,15 +127,10 @@ const AdminUser = Discourse.User.extend({
},
grantAdmin() {
const self = this;
return ajax("/admin/users/" + this.get('id') + "/grant_admin", {
return ajax(`/admin/users/${this.get('id')}/grant_admin`, {
type: 'PUT'
}).then(function() {
self.setProperties({
admin: true,
can_grant_admin: false,
can_revoke_admin: true
});
}).then(() => {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
}).catch(popupAjaxError);
},
@ -346,7 +341,7 @@ const AdminUser = Discourse.User.extend({
},
sendActivationEmail() {
return ajax('/users/action/send_activation_email', {
return ajax(userPath('action/send_activation_email'), {
type: 'POST',
data: { username: this.get('username') }
}).then(function() {

View File

@ -22,18 +22,40 @@
<div class='display-row username'>
<div class='field'>{{i18n 'user.username.title'}}</div>
<div class='value'>{{model.username}}</div>
<div class='value'>
{{#if editingUsername}}
{{text-field value=userUsernameValue autofocus="autofocus"}}
{{else}}
<span {{action "toggleUsernameEdit"}}>{{model.username}}</span>
{{/if}}
</div>
<div class='controls'>
{{#link-to 'preferences.username' model class="btn"}}
{{fa-icon "pencil"}}
{{i18n 'user.change_username.title'}}
{{/link-to}}
{{#if editingUsername}}
{{d-button action="saveUsername" label="admin.user_fields.save"}}
<a href {{action "toggleUsernameEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button action="toggleUsernameEdit" icon="pencil"}}
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'user.name.title'}}</div>
<div class='value'>{{model.name}}</div>
<div class='value'>
{{#if editingName}}
{{text-field value=userNameValue autofocus="autofocus"}}
{{else}}
<span {{action "toggleNameEdit"}}>{{model.name}}</span>
{{/if}}
</div>
<div class='controls'>
{{#if editingName}}
{{d-button action="saveName" label="admin.user_fields.save"}}
<a href {{action "toggleNameEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button action="toggleNameEdit" icon="pencil"}}
{{/if}}
</div>
</div>
{{#if canCheckEmails}}
@ -90,10 +112,10 @@
</div>
<div class='controls'>
{{#if editingTitle}}
{{d-button action="saveTitle" label="admin.user.save_title"}}
{{d-button action="saveTitle" label="admin.user_fields.save"}}
<a href {{action "toggleTitleEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button action="toggleTitleEdit" icon="pencil" label="admin.user.edit_title"}}
{{d-button action="toggleTitleEdit" icon="pencil"}}
{{/if}}
</div>
</div>

View File

@ -25,7 +25,7 @@
//= require ./discourse/lib/computed
//= require ./discourse/lib/formatter
//= require ./discourse/lib/eyeline
//= require ./discourse/mixins/scrolling
//= require ./discourse/lib/show-modal
//= require ./discourse/mixins/scrolling
//= require ./discourse/models/model
//= require ./discourse/models/rest
@ -69,7 +69,6 @@
//= require ./discourse/lib/emoji/groups
//= require ./discourse/lib/emoji/toolbar
//= require ./discourse/components/d-editor
//= require ./discourse/lib/show-modal
//= require ./discourse/lib/screen-track
//= require ./discourse/routes/discourse
//= require ./discourse/routes/build-topic-route

View File

@ -1,46 +0,0 @@
import computed from "ember-addons/ember-computed-decorators";
import { observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
limited: false,
autoCloseValid: false,
@computed("limited")
autoCloseUnits(limited) {
const key = limited ? "composer.auto_close.limited.units" : "composer.auto_close.all.units";
return I18n.t(key);
},
@computed("limited")
autoCloseExamples(limited) {
const key = limited ? "composer.auto_close.limited.examples" : "composer.auto_close.all.examples";
return I18n.t(key);
},
@observes("autoCloseTime", "limited")
_updateAutoCloseValid() {
const limited = this.get("limited"),
autoCloseTime = this.get("autoCloseTime"),
isValid = this._isAutoCloseValid(autoCloseTime, limited);
this.set("autoCloseValid", isValid);
},
_isAutoCloseValid(autoCloseTime, limited) {
const t = (autoCloseTime || "").toString().trim();
if (t.length === 0) {
// "empty" is always valid
return true;
} else if (limited) {
// only # of hours in limited mode
return t.match(/^(\d+\.)?\d+$/);
} else {
if (t.match(/^\d{4}-\d{1,2}-\d{1,2}(?: \d{1,2}:\d{2}(\s?[AP]M)?){0,1}$/i)) {
// timestamp must be in the future
return moment(t).isAfter();
} else {
// either # of hours or absolute time
return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null;
}
}
}
});

View File

@ -0,0 +1,47 @@
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
limited: false,
didInsertElement() {
this._super();
this._updateInputValid();
},
@computed("limited")
inputUnitsKey(limited) {
return limited ? "topic.auto_update_input.limited.units" : "topic.auto_update_input.all.units";
},
@computed("limited")
inputExamplesKey(limited) {
return limited ? "topic.auto_update_input.limited.examples" : "topic.auto_update_input.all.examples";
},
@observes("input", "limited")
_updateInputValid() {
this.set(
"inputValid", this._isInputValid(this.get("input"), this.get("limited"))
);
},
_isInputValid(input, limited) {
const t = (input || "").toString().trim();
if (t.length === 0) {
return true;
// "empty" is always valid
} else if (limited) {
// only # of hours in limited mode
return t.match(/^(\d+\.)?\d+$/);
} else {
if (t.match(/^\d{4}-\d{1,2}-\d{1,2}(?: \d{1,2}:\d{2}(\s?[AP]M)?){0,1}$/i)) {
// timestamp must be in the future
return moment(t).isAfter();
} else {
// either # of hours or absolute time
return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null;
}
}
}
});

View File

@ -3,6 +3,7 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import computed from 'ember-addons/ember-computed-decorators';
import { observes, on } from 'ember-addons/ember-computed-decorators';
import PermissionType from 'discourse/models/permission-type';
import Category from 'discourse/models/category';
export default ComboboxView.extend({
classNames: ['combobox category-combobox'],
@ -14,13 +15,16 @@ export default ComboboxView.extend({
content(scopedCategoryId, categories) {
// Always scope to the parent of a category, if present
if (scopedCategoryId) {
const scopedCat = Discourse.Category.findById(scopedCategoryId);
const scopedCat = Category.findById(scopedCategoryId);
scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id');
}
const excludeCategoryId = this.get('excludeCategoryId');
return categories.filter(c => {
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory')) { return false; }
const categoryId = c.get('id');
if (scopedCategoryId && categoryId !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory') || excludeCategoryId === categoryId) { return false; }
return c.get('permission') === PermissionType.FULL;
});
},
@ -30,19 +34,19 @@ export default ComboboxView.extend({
_updateCategories() {
if (!this.get('categories')) {
const categories = Discourse.SiteSettings.fixed_category_positions_on_create ?
Discourse.Category.list() :
Discourse.Category.listByActivity();
Category.list() :
Category.listByActivity();
this.set('categories', categories);
}
},
@computed("rootNone")
none(rootNone) {
@computed("rootNone", "rootNoneLabel")
none(rootNone, rootNoneLabel) {
if (Discourse.SiteSettings.allow_uncategorized_topics || this.get('allowUncategorized')) {
if (rootNone) {
return "category.none";
return rootNoneLabel || "category.none";
} else {
return Discourse.Category.findUncategorized();
return Category.findUncategorized();
}
} else {
return 'category.choose';
@ -54,12 +58,12 @@ export default ComboboxView.extend({
// If we have no id, but text with the uncategorized name, we can use that badge.
if (Ember.isEmpty(item.id)) {
const uncat = Discourse.Category.findUncategorized();
const uncat = Category.findUncategorized();
if (uncat && uncat.get('name') === item.text) {
category = uncat;
}
} else {
category = Discourse.Category.findById(parseInt(item.id,10));
category = Category.findById(parseInt(item.id,10));
}
if (!category) return item.text;
@ -67,7 +71,7 @@ export default ComboboxView.extend({
const parentCategoryId = category.get('parent_category_id');
if (parentCategoryId) {
result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + "&nbsp;" + result;
result = categoryBadgeHTML(Category.findById(parentCategoryId), {link: false}) + "&nbsp;" + result;
}
result += ` <span class='topic-count'>&times; ${category.get('topic_count')}</span>`;

View File

@ -3,6 +3,7 @@ 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';
import Composer from 'discourse/models/composer';
import { load } from 'pretty-text/oneboxer';
import { ajax } from 'discourse/lib/ajax';
import InputValidation from 'discourse/models/input-validation';
@ -138,7 +139,7 @@ export default Ember.Component.extend({
_renderUnseenMentions($preview, unseen) {
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
fetchUnseenMentions(unseen, this.get('topic.id')).then(() => {
fetchUnseenMentions(unseen, this.get('composer.topic.id')).then(() => {
linkSeenMentions($preview, this.siteSettings);
this._warnMentionedGroups($preview);
this._warnCannotSeeMention($preview);
@ -187,13 +188,25 @@ export default Ember.Component.extend({
},
_warnCannotSeeMention($preview) {
const composerDraftKey = this.get('composer.draftKey');
if (composerDraftKey === Composer.CREATE_TOPIC ||
composerDraftKey === Composer.NEW_PRIVATE_MESSAGE_KEY ||
composerDraftKey === Composer.REPLY_AS_NEW_TOPIC_KEY ||
composerDraftKey === Composer.REPLY_AS_NEW_PRIVATE_MESSAGE_KEY) {
return;
}
Ember.run.scheduleOnce('afterRender', () => {
var found = this.get('warnedCannotSeeMentions') || [];
let found = this.get('warnedCannotSeeMentions') || [];
$preview.find('.mention.cannot-see').each((idx,e) => {
const $e = $(e);
var name = $e.data('name');
let name = $e.data('name');
if (found.indexOf(name) === -1) {
this.sendAction('cannotSeeMention', [{name: name}]);
this.sendAction('cannotSeeMention', [{ name: name }]);
found.push(name);
}
});

View File

@ -37,7 +37,7 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
const enteredAt = this.get('enteredAt');
if (enteredAt && (this.get('lastEnteredAt') !== enteredAt)) {
this._lastShowTopic = null;
this.scrolled();
Ember.run.schedule('afterRender', () => this.scrolled());
this.set('lastEnteredAt', enteredAt);
}
},
@ -131,18 +131,22 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
}
}
this.set('hasScrolled', offset > 0);
const topic = this.get('topic');
const showTopic = this.showTopicInHeader(topic, offset);
if (showTopic !== this._lastShowTopic) {
this._lastShowTopic = showTopic;
if (showTopic) {
this.appEvents.trigger('header:show-topic', topic);
this._lastShowTopic = true;
} else {
if (!DiscourseURL.isJumpScheduled()) {
this.appEvents.trigger('header:hide-topic');
const loadingNear = topic.get('postStream.loadingNearPost') || 1;
if (loadingNear === 1) {
this.appEvents.trigger('header:hide-topic');
this._lastShowTopic = false;
}
}
}
}

View File

@ -22,7 +22,7 @@ export default DropdownButton.extend({
id: l.id,
title: I18n.t(`${start}.title`),
description: I18n.t(`${start}.description`),
styleClasses: `${l.key} fa fa-${l.icon}`
styleClasses: `${l.key.dasherize()} fa fa-${l.icon}`
};
});
},
@ -31,7 +31,7 @@ export default DropdownButton.extend({
text(notificationLevel) {
const details = buttonDetails(notificationLevel);
const { key } = details;
const icon = iconHTML(details.icon, { class: key });
const icon = iconHTML(details.icon, { class: key.dasherize() });
if (this.get('buttonIncludesText')) {
const prefix = this.get('i18nPrefix');

View File

@ -55,7 +55,8 @@ export default Ember.Component.extend({
const { isIOS, isAndroid, isSafari } = this.capabilities;
const showAtEnd = isMobileDevice || isIOS || isAndroid;
// used to work around Safari losing selection
// Don't mess with the original range as it results in weird behaviours
// where certain browsers will deselect the selection
const clone = firstRange.cloneRange();
// create a marker element containing a single invisible character
@ -63,9 +64,9 @@ export default Ember.Component.extend({
markerElement.appendChild(document.createTextNode("\ufeff"));
// on mobile, collapse the range at the end of the selection
if (showAtEnd) { firstRange.collapse(); }
if (showAtEnd) { clone.collapse(); }
// insert the marker
firstRange.insertNode(markerElement);
clone.insertNode(markerElement);
// retrieve the position of the marker
const $markerElement = $(markerElement);

View File

@ -1,4 +1,5 @@
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
import { userPath } from 'discourse/lib/url';
export function actionDescriptionHtml(actionCode, createdAt, username) {
const dt = new Date(createdAt);
@ -9,7 +10,7 @@ export function actionDescriptionHtml(actionCode, createdAt, username) {
if (actionCode === "invited_group" || actionCode === "removed_group") {
who = `<a class="mention-group" href="/groups/${username}">@${username}</a>`;
} else {
who = `<a class="mention" href="/users/${username}">@${username}</a>`;
who = `<a class="mention" href="${userPath(username)}">@${username}</a>`;
}
}
return I18n.t(`action_codes.${actionCode}`, { who, when }).htmlSafe();

View File

@ -1,51 +0,0 @@
import { bufferedRender } from 'discourse-common/lib/buffered-render';
export default Ember.Component.extend(bufferedRender({
elementId: 'topic-closing-info',
delayedRerender: null,
rerenderTriggers: ['topic.closed',
'topic.details.auto_close_at',
'topic.details.auto_close_based_on_last_post',
'topic.details.auto_close_hours'],
buildBuffer(buffer) {
if (!!Ember.isEmpty(this.get('topic.details.auto_close_at'))) return;
if (this.get("topic.closed")) return;
var autoCloseAt = moment(this.get('topic.details.auto_close_at'));
if (autoCloseAt < new Date()) return;
var duration = moment.duration(autoCloseAt - moment());
var minutesLeft = duration.asMinutes();
var timeLeftString = duration.humanize(true);
var rerenderDelay = 1000;
if (minutesLeft > 2160) {
rerenderDelay = 12 * 60 * 60000;
} else if (minutesLeft > 1410) {
rerenderDelay = 60 * 60000;
} else if (minutesLeft > 90) {
rerenderDelay = 30 * 60000;
} else if (minutesLeft > 2) {
rerenderDelay = 60000;
}
var basedOnLastPost = this.get("topic.details.auto_close_based_on_last_post");
var key = basedOnLastPost ? 'topic.auto_close_notice_based_on_last_post' : 'topic.auto_close_notice';
var autoCloseHours = this.get("topic.details.auto_close_hours") || 0;
buffer.push('<h3><i class="fa fa-clock-o"></i> ');
buffer.push( I18n.t(key, { timeLeft: timeLeftString, duration: moment.duration(autoCloseHours, "hours").humanize() }) );
buffer.push('</h3>');
// TODO Sam: concerned this can cause a heavy rerender loop
this.set('delayedRerender', Em.run.later(this, this.rerender, rerenderDelay));
},
willDestroyElement() {
if( this.delayedRerender ) {
Em.run.cancel(this.get('delayedRerender'));
}
}
}));

View File

@ -94,11 +94,15 @@ export default Ember.Component.extend(CleansUp, {
actions: {
enterTop() {
DiscourseURL.routeTo(this.get('topic.url'));
const topic = this.get('topic');
this.appEvents.trigger('header:update-topic', topic);
DiscourseURL.routeTo(topic.get('url'));
},
enterBottom() {
DiscourseURL.routeTo(this.get('topic.lastPostUrl'));
const topic = this.get('topic');
this.appEvents.trigger('header:update-topic', topic);
DiscourseURL.routeTo(topic.get('lastPostUrl'));
}
}
});

View File

@ -3,7 +3,12 @@ import showModal from 'discourse/lib/show-modal';
export default Ember.Component.extend({
composerOpen: null,
info: Em.Object.create(),
info: null,
init() {
this._super();
this.set('info', Ember.Object.create());
},
_performCheckSize() {
if (!this.element || this.isDestroying || this.isDestroyed) { return; }

View File

@ -0,0 +1,78 @@
import { bufferedRender } from 'discourse-common/lib/buffered-render';
import Category from 'discourse/models/category';
export default Ember.Component.extend(bufferedRender({
elementId: 'topic-status-info',
delayedRerender: null,
rerenderTriggers: [
'topic.topic_status_update',
'topic.topic_status_update.execute_at',
'topic.topic_status_update.based_on_last_post',
'topic.topic_status_update.duration',
'topic.topic_status_update.category_id',
],
buildBuffer(buffer) {
if (!this.get('topic.topic_status_update.execute_at')) return;
let statusUpdateAt = moment(this.get('topic.topic_status_update.execute_at'));
if (statusUpdateAt < new Date()) return;
let duration = moment.duration(statusUpdateAt - moment());
let minutesLeft = duration.asMinutes();
let rerenderDelay = 1000;
if (minutesLeft > 2160) {
rerenderDelay = 12 * 60 * 60000;
} else if (minutesLeft > 1410) {
rerenderDelay = 60 * 60000;
} else if (minutesLeft > 90) {
rerenderDelay = 30 * 60000;
} else if (minutesLeft > 2) {
rerenderDelay = 60000;
}
let autoCloseHours = this.get("topic.topic_status_update.duration") || 0;
buffer.push('<h3><i class="fa fa-clock-o"></i> ');
let options = {
timeLeft: duration.humanize(true),
duration: moment.duration(autoCloseHours, "hours").humanize(),
};
const categoryId = this.get('topic.topic_status_update.category_id');
if (categoryId) {
const category = Category.findById(categoryId);
options = _.assign({
categoryName: category.get('slug'),
categoryUrl: category.get('url')
}, options);
}
buffer.push(I18n.t(this._noticeKey(), options));
buffer.push('</h3>');
// TODO Sam: concerned this can cause a heavy rerender loop
this.set('delayedRerender', Em.run.later(this, this.rerender, rerenderDelay));
},
willDestroyElement() {
if( this.delayedRerender ) {
Em.run.cancel(this.get('delayedRerender'));
}
},
_noticeKey() {
const statusType = this.get('topic.topic_status_update.status_type');
if (this.get("topic.topic_status_update.based_on_last_post")) {
return `topic.status_update_notice.auto_${statusType}_based_on_last_post`;
} else {
return `topic.status_update_notice.auto_${statusType}`;
}
}
}));

View File

@ -5,6 +5,7 @@ import afterTransition from 'discourse/lib/after-transition';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import DiscourseURL from 'discourse/lib/url';
import User from 'discourse/models/user';
import { userPath } from 'discourse/lib/url';
const clickOutsideEventName = "mousedown.outside-user-card";
const clickDataExpand = "click.discourse-user-card";
@ -92,7 +93,7 @@ export default Ember.Component.extend(CleansUp, {
// Don't show on mobile
if (this.site.mobileView) {
DiscourseURL.routeTo(`/users/${username}`);
DiscourseURL.routeTo(userPath(username));
return false;
}

View File

@ -1,5 +1,5 @@
import { url } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
import { userPath } from 'discourse/lib/url';
function normalize(name) {
return name.replace(/[\-\_ \.]/g, '').toLowerCase();
@ -8,7 +8,11 @@ function normalize(name) {
export default Ember.Component.extend({
classNameBindings: [':user-info', 'size'],
size: 'small',
userPath: url('user.username', '/users/%@'),
@computed('user.username')
userPath(username) {
return userPath(username);
},
// TODO: In later ember releases `hasBlock` works without this
hasBlock: Ember.computed.alias('template'),

View File

@ -0,0 +1,36 @@
import computed from 'ember-addons/ember-computed-decorators';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { ajax } from 'discourse/lib/ajax';
import { extractError } from 'discourse/lib/ajax-error';
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(ModalFunctionality, {
login: Ember.inject.controller(),
currentEmail: null,
newEmail: null,
password: null,
@computed('newEmail', 'currentEmail')
submitDisabled(newEmail, currentEmail) {
return newEmail === currentEmail;
},
actions: {
changeEmail() {
const login = this.get('login');
ajax(userPath('update-activation-email'), {
data: {
username: login.get('loginName'),
password: login.get('loginPassword'),
email: this.get('newEmail')
},
type: 'PUT'
}).then(() => {
const modal = this.showModal('activation-resent', {title: 'log_in'});
modal.set('currentEmail', this.get('newEmail'));
}).catch(err => this.flash(extractError(err), 'error'));
}
}
});

View File

@ -0,0 +1,5 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
modal: null
});

View File

@ -367,7 +367,7 @@ export default Ember.Controller.extend({
cannotSeeMention(mentions) {
mentions.forEach(mention => {
const translation = (this.get('topic.isPrivateMessage')) ?
const translation = (this.get('model.topic.isPrivateMessage')) ?
'composer.cannot_see_mention.private' :
'composer.cannot_see_mention.category';
const body = I18n.t(translation, {

View File

@ -6,6 +6,7 @@ import { emailValid } from 'discourse/lib/utilities';
import InputValidation from 'discourse/models/input-validation';
import PasswordValidation from "discourse/mixins/password-validation";
import UsernameValidation from "discourse/mixins/username-validation";
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, UsernameValidation, {
login: Ember.inject.controller(),
@ -164,7 +165,7 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U
@on('init')
fetchConfirmationValue() {
return ajax('/users/hp.json').then(json => {
return ajax(userPath('hp.json')).then(json => {
this.set('accountPasswordConfirm', json.value);
this.set('accountChallenge', json.challenge.split("").reverse().join(""));
});
@ -196,7 +197,7 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U
const $hidden_login_form = $('#hidden-login-form');
$hidden_login_form.find('input[name=username]').val(attrs.accountUsername);
$hidden_login_form.find('input[name=password]').val(attrs.accountPassword);
$hidden_login_form.find('input[name=redirect]').val(Discourse.getURL('/users/account-created'));
$hidden_login_form.find('input[name=redirect]').val(userPath('account-created'));
$hidden_login_form.submit();
} else {
self.flash(result.message || I18n.t('create_account.failed'), 'error');

View File

@ -3,6 +3,7 @@ import { queryParams } from 'discourse/controllers/discovery-sortable';
import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection';
import { endWith } from 'discourse/lib/computed';
import showModal from 'discourse/lib/show-modal';
import { userPath } from 'discourse/lib/url';
const controllerOpts = {
discovery: Ember.inject.controller(),
@ -133,14 +134,14 @@ const controllerOpts = {
}.property('allLoaded', 'model.topics.length'),
footerEducation: function() {
if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !Discourse.User.current()) { return; }
if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !this.currentUser) { return; }
const split = (this.get('model.filter') || '').split('/');
if (split[0] !== 'new' && split[0] !== 'unread') { return; }
return I18n.t("topics.none.educate." + split[0], {
userPrefsUrl: Discourse.getURL("/users/") + (Discourse.User.currentProp("username_lower")) + "/preferences"
userPrefsUrl: userPath(`${this.currentUser.get('username_lower')}/preferences`)
});
}.property('allLoaded', 'model.topics.length')

View File

@ -1,78 +0,0 @@
import { ajax } from 'discourse/lib/ajax';
import { observes } from "ember-addons/ember-computed-decorators";
import ModalFunctionality from 'discourse/mixins/modal-functionality';
// Modal related to auto closing of topics
export default Ember.Controller.extend(ModalFunctionality, {
auto_close_valid: true,
auto_close_invalid: Em.computed.not('auto_close_valid'),
disable_submit: Em.computed.or('auto_close_invalid', 'loading'),
loading: false,
@observes("model.details.auto_close_at", "model.details.auto_close_hours")
setAutoCloseTime() {
let autoCloseTime = null;
if (this.get("model.details.auto_close_based_on_last_post")) {
autoCloseTime = this.get("model.details.auto_close_hours");
} else if (this.get("model.details.auto_close_at")) {
const closeTime = new Date(this.get("model.details.auto_close_at"));
if (closeTime > new Date()) {
autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm");
}
}
this.set("model.auto_close_time", autoCloseTime);
},
actions: {
saveAutoClose() { this.setAutoClose(this.get("model.auto_close_time")); },
removeAutoClose() { this.setAutoClose(null); }
},
setAutoClose(time) {
const self = this;
this.set('loading', true);
ajax({
url: `/t/${this.get('model.id')}/autoclose`,
type: 'PUT',
dataType: 'json',
data: {
auto_close_time: time,
auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"),
timezone_offset: (new Date().getTimezoneOffset())
}
}).then(result => {
self.set('loading', false);
if (result.success) {
this.send('closeModal');
this.set('model.details.auto_close_at', result.auto_close_at);
this.set('model.details.auto_close_hours', result.auto_close_hours);
} else {
bootbox.alert(I18n.t('composer.auto_close.error'));
}
}).catch(() => {
// TODO - incorrectly responds to network errors as bad input
bootbox.alert(I18n.t('composer.auto_close.error'));
self.set('loading', false);
});
},
willCloseImmediately: function() {
if (!this.get('model.details.auto_close_based_on_last_post')) {
return false;
}
let closeDate = new Date(this.get('model.last_posted_at'));
closeDate.setHours(closeDate.getHours() + this.get('model.auto_close_time'));
return closeDate < new Date();
}.property('model.details.auto_close_based_on_last_post', 'model.auto_close_time', 'model.last_posted_at'),
willCloseI18n: function() {
if (this.get('model.details.auto_close_based_on_last_post')) {
let closeDate = new Date(this.get('model.last_posted_at'));
let diff = Math.round((new Date() - closeDate)/(1000*60*60));
return I18n.t('topic.auto_close_immediate', {count: diff});
}
}.property('model.details.auto_close_based_on_last_post', 'model.last_posted_at')
});

View File

@ -0,0 +1,122 @@
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import TopicStatusUpdate from 'discourse/models/topic-status-update';
import { popupAjaxError } from 'discourse/lib/ajax-error';
const CLOSE_STATUS_TYPE = 'close';
const OPEN_STATUS_TYPE = 'open';
const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
export default Ember.Controller.extend(ModalFunctionality, {
closeStatusType: CLOSE_STATUS_TYPE,
openStatusType: OPEN_STATUS_TYPE,
publishToCategoryStatusType: PUBLISH_TO_CATEGORY_STATUS_TYPE,
updateTimeValid: null,
updateTimeInvalid: Em.computed.not('updateTimeValid'),
loading: false,
updateTime: null,
topicStatusUpdate: Ember.computed.alias("model.topic_status_update"),
selection: Ember.computed.alias('model.topic_status_update.status_type'),
autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE),
autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE),
publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE),
@computed('autoClose', 'updateTime')
disableAutoClose(autoClose, updateTime) {
return updateTime && !autoClose;
},
@computed('autoOpen', 'updateTime')
disableAutoOpen(autoOpen, updateTime) {
return updateTime && !autoOpen;
},
@computed('publishToCatgory', 'updateTime')
disablePublishToCategory(publishToCatgory, updateTime) {
return updateTime && !publishToCatgory;
},
@computed('topicStatusUpdate.based_on_last_post', 'updateTime', 'model.last_posted_at')
willCloseImmediately(basedOnLastPost, updateTime, lastPostedAt) {
if (!basedOnLastPost) {
return false;
}
const closeDate = new Date(lastPostedAt);
closeDate.setHours(closeDate.getHours() + updateTime);
return closeDate < new Date();
},
@computed('topicStatusUpdate.based_on_last_post', 'model.last_posted_at')
willCloseI18n(basedOnLastPost, lastPostedAt) {
if (basedOnLastPost) {
const diff = Math.round((new Date() - new Date(lastPostedAt)) / (1000*60*60));
return I18n.t('topic.auto_close_immediate', { count: diff });
}
},
@computed('updateTime', 'updateTimeInvalid', 'loading')
saveDisabled(updateTime, updateTimeInvalid, loading) {
return Ember.isEmpty(updateTime) || updateTimeInvalid || loading;
},
@computed("model.visible")
excludeCategoryId(visible) {
if (visible) return this.get('model.category_id');
},
@observes("topicStatusUpdate.execute_at", "topicStatusUpdate.duration")
_setUpdateTime() {
let time = null;
if (this.get("topicStatusUpdate.based_on_last_post")) {
time = this.get("topicStatusUpdate.duration");
} else if (this.get("topicStatusUpdate.execute_at")) {
const closeTime = new Date(this.get("topicStatusUpdate.execute_at"));
if (closeTime > new Date()) {
time = moment(closeTime).format("YYYY-MM-DD HH:mm");
}
}
this.set("updateTime", time);
},
_setStatusUpdate(time, status_type) {
this.set('loading', true);
TopicStatusUpdate.updateStatus(
this.get('model.id'),
time,
this.get('topicStatusUpdate.based_on_last_post'),
status_type,
this.get('categoryId')
).then(result => {
if (time) {
this.send('closeModal');
this.get("topicStatusUpdate").setProperties({
execute_at: result.execute_at,
duration: result.duration,
category_id: result.category_id
});
this.set('model.closed', result.closed);
} else {
this.set('topicStatusUpdate', Ember.Object.create({}));
this.set('selection', null);
}
}).catch(error => {
popupAjaxError(error);
}).finally(() => this.set('loading', false));
},
actions: {
saveStatusUpdate() {
this._setStatusUpdate(this.get("updateTime"), this.get('selection'));
},
removeStatusUpdate() {
this._setStatusUpdate(null, this.get('selection'));
}
}
});

View File

@ -11,6 +11,8 @@ const SortOrders = [
{name: I18n.t('search.latest_post'), id: 1, term: 'order:latest'},
{name: I18n.t('search.most_liked'), id: 2, term: 'order:likes'},
{name: I18n.t('search.most_viewed'), id: 3, term: 'order:views'},
{name: I18n.t('search.latest_topic'), id: 4, term: 'order:latest_topic'},
];
export default Ember.Controller.extend({
@ -73,14 +75,7 @@ export default Ember.Controller.extend({
@computed('q')
noSortQ(q) {
if (q) {
SortOrders.forEach((order) => {
if (q.indexOf(order.term) > -1){
q = q.replace(order.term, "");
q = q.trim();
}
});
}
q = this.cleanTerm(q);
return escapeExpression(q);
},
@ -88,17 +83,23 @@ export default Ember.Controller.extend({
setSearchTerm(term) {
this._searchOnSortChange = false;
term = this.cleanTerm(term);
this._searchOnSortChange = true;
this.set('searchTerm', term);
},
cleanTerm(term) {
if (term) {
SortOrders.forEach(order => {
if (term.indexOf(order.term) > -1){
let matches = term.match(new RegExp(`${order.term}\\b`));
if (matches) {
this.set('sortOrder', order.id);
term = term.replace(order.term, "");
term = term.replace(new RegExp(`${order.term}\\b`, 'g'), "");
term = term.trim();
}
});
}
this._searchOnSortChange = true;
this.set('searchTerm', term);
return term;
},
@observes('sortOrder')

View File

@ -1,23 +1,26 @@
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(ModalFunctionality, {
emailSent: false,
onShow() {
this.set("emailSent", false);
},
actions: {
sendActivationEmail() {
ajax('/users/action/send_activation_email', {
ajax(userPath('action/send_activation_email'), {
data: { username: this.get('username') },
type: 'POST'
}).then(() => {
this.set('emailSent', true);
const modal = this.showModal('activation-resent', {title: 'log_in'});
modal.set('currentEmail', this.get('currentEmail'));
}).catch(popupAjaxError);
},
editActivationEmail() {
const modal = this.showModal('activation-edit', {title: 'login.change_email'});
const currentEmail = this.get('currentEmail');
modal.set('currentEmail', currentEmail);
modal.set('newEmail', currentEmail);
}
}
});

View File

@ -3,6 +3,7 @@ import getUrl from 'discourse-common/lib/get-url';
import DiscourseURL from 'discourse/lib/url';
import { ajax } from 'discourse/lib/ajax';
import PasswordValidation from "discourse/mixins/password-validation";
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(PasswordValidation, {
isDeveloper: Ember.computed.alias('model.is_developer'),
@ -27,7 +28,7 @@ export default Ember.Controller.extend(PasswordValidation, {
actions: {
submit() {
ajax({
url: `/users/password-reset/${this.get('model.token')}.json`,
url: userPath(`password-reset/${this.get('model.token')}.json`),
type: 'PUT',
data: {
password: this.get('accountPassword')

View File

@ -1,5 +1,6 @@
import { setting, propertyEqual } from 'discourse/lib/computed';
import DiscourseURL from 'discourse/lib/url';
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend({
taken: false,
@ -48,7 +49,7 @@ export default Ember.Controller.extend({
if (result) {
this.set('saving', true);
this.get('content').changeUsername(this.get('newUsername')).then(() => {
DiscourseURL.redirectTo("/users/" + this.get('newUsername').toLowerCase() + "/preferences");
DiscourseURL.redirectTo(userPath(this.get('newUsername').toLowerCase() + "/preferences"));
})
.catch(() => this.set('error', true))
.finally(() => this.set('saving', false));

View File

@ -1,5 +1,6 @@
import { ajax } from 'discourse/lib/ajax';
import computed from 'ember-addons/ember-computed-decorators';
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend({
application: Ember.inject.controller(),
@ -18,7 +19,7 @@ export default Ember.Controller.extend({
markFaqRead() {
const currentUser = this.currentUser;
if (currentUser) {
ajax("/users/read-faq", { method: "POST" }).then(() => {
ajax(userPath("read-faq"), { method: "POST" }).then(() => {
currentUser.set('read_faq', true);
});
}

View File

@ -12,6 +12,7 @@ import Post from 'discourse/models/post';
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';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composer: Ember.inject.controller(),
@ -126,7 +127,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
gotoInbox(name) {
var url = '/users/' + this.get('currentUser.username_lower') + '/messages';
let url = userPath(this.get('currentUser.username_lower') + '/messages');
if (name) {
url = url + '/group/' + name;
}
@ -160,10 +161,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return this.send(name, model);
},
openAutoClose() {
this.send('showAutoClose');
},
openFeatureTopic() {
this.send('showFeatureTopic');
},
@ -591,7 +588,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
},
toggleClosed() {
this.get('content').toggleStatus('closed');
const topic = this.get('content');
this.get('content').toggleStatus('closed').then(result => {
topic.set('topic_status_update', result.topic_status_update);
});
},
recoverTopic() {
@ -867,7 +868,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
const refresh = (args) => this.appEvents.trigger('post-stream:refresh', args);
this.messageBus.subscribe("/topic/" + this.get('model.id'), data => {
this.messageBus.subscribe(`/topic/${this.get('model.id')}`, data => {
const topic = this.get('model');
if (data.notification_level_change) {
@ -877,9 +878,24 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}
const postStream = this.get('model.postStream');
if (data.reload_topic) {
topic.reload().then(() => {
this.send('postChangedRoute', topic.get('post_number') || 1);
this.appEvents.trigger('header:update-topic', topic);
if (data.refresh_stream) postStream.refresh();
});
return;
}
switch (data.type) {
case "acted":
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id, refreshLikes: true }));
postStream.triggerChangedPost(
data.id,
data.updated_at,
{ preserveCooked: true }
).then(() => refresh({ id: data.id, refreshLikes: true }));
break;
case "revised":
case "rebaked": {
@ -914,27 +930,20 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}
}
if (data.reload_topic) {
topic.reload().then(() => {
this.send('postChangedRoute', topic.get('post_number') || 1);
this.appEvents.trigger('header:update-topic', topic);
});
} else {
if (topic.get('isPrivateMessage') &&
this.currentUser &&
this.currentUser.get('id') !== data.user_id &&
data.type === 'created') {
if (topic.get('isPrivateMessage') &&
this.currentUser &&
this.currentUser.get('id') !== data.user_id &&
data.type === 'created') {
const postNumber = data.post_number;
const notInPostStream = topic.get('highest_post_number') <= postNumber;
const postNumberDifference = postNumber - topic.get('currentPost');
const postNumber = data.post_number;
const notInPostStream = topic.get('highest_post_number') <= postNumber;
const postNumberDifference = postNumber - topic.get('currentPost');
if (notInPostStream &&
postNumberDifference > 0 &&
postNumberDifference < 7) {
if (notInPostStream &&
postNumberDifference > 0 &&
postNumberDifference < 7) {
this._scrollToPost(data.post_number);
}
this._scrollToPost(data.post_number);
}
}
});

View File

@ -12,12 +12,14 @@ export default {
DiscourseURL.rewrite(/^\/category\//, "/c/");
DiscourseURL.rewrite(/^\/group\//, "/groups/");
DiscourseURL.rewrite(/\/private-messages\/$/, "/messages/");
DiscourseURL.rewrite(/^\/users$/, "/u");
DiscourseURL.rewrite(/^\/users\//, "/u/");
if (currentUser) {
const username = currentUser.get('username');
DiscourseURL.rewrite(new RegExp(`^/users/${username}/?$`, "i"), `/users/${username}/activity`);
DiscourseURL.rewrite(new RegExp(`^/u/${username}/?$`, "i"), `/u/${username}/activity`);
}
DiscourseURL.rewrite(/^\/users\/([^\/]+)\/?$/, "/users/$1/activity");
DiscourseURL.rewrite(/^\/u\/([^\/]+)\/?$/, "/u/$1/summary");
}
};

View File

@ -66,12 +66,10 @@ const DiscourseLocation = Ember.Object.extend({
getURL() {
const location = get(this, 'location');
let url = location.pathname;
url = url.replace(Discourse.BaseUri, '');
const search = location.search || '';
url += search;
return url;
},

View File

@ -1,4 +1,5 @@
import { ajax } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
function replaceSpan($e, username, opts) {
let extra = "";
@ -15,7 +16,7 @@ function replaceSpan($e, username, opts) {
extra = `data-name='${username}'`;
extraClass = "cannot-see";
}
$e.replaceWith(`<a href='${Discourse.getURL("/users/") + username.toLowerCase()}' class='mention ${extraClass}' ${extra}>@${username}</a>`);
$e.replaceWith(`<a href='${userPath(username.toLowerCase())}' class='mention ${extraClass}' ${extra}>@${username}</a>`);
}
}
@ -54,7 +55,7 @@ export function linkSeenMentions($elem, siteSettings) {
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
export function fetchUnseenMentions(usernames, topic_id) {
return ajax("/users/is_local_username", { data: { usernames, topic_id } }).then(r => {
return ajax(userPath("is_local_username"), { data: { usernames, topic_id } }).then(r => {
r.valid.forEach(v => found[v] = true);
r.valid_groups.forEach(vg => foundGroups[vg] = true);
r.mentionable_groups.forEach(mg => mentionableGroups[mg.name] = mg);

View File

@ -55,6 +55,8 @@ export default class LockOn {
const interval = setInterval(() => {
let top = this.elementTop();
if (top < 0) { top = 0; }
const scrollTop = $(window).scrollTop();
if (typeof(top) === "undefined" || isNaN(top)) {

View File

@ -18,9 +18,11 @@ import { addTagsHtmlCallback } from 'discourse/lib/render-tags';
import { addUserMenuGlyph } from 'discourse/widgets/user-menu';
import { addPostClassesCallback } from 'discourse/widgets/post';
import { addPostTransformCallback } from 'discourse/widgets/post-stream';
import { attachAdditionalPanel } from 'discourse/widgets/header';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = '0.8.5';
const PLUGIN_API_VERSION = '0.8.6';
class PluginApi {
constructor(version, container) {
@ -333,6 +335,26 @@ class PluginApi {
return addFlagProperty(property);
}
/**
* Adds a panel to the header
*
* takes a widget name, a value to toggle on, and a function which returns the attrs for the widget
* Example:
* ```javascript
* api.addHeaderPanel('widget-name', 'widgetVisible', function(attrs, state) {
* return { name: attrs.name, description: state.description };
* });
* ```
* 'toggle' is an attribute on the state of the header widget,
*
* 'transformAttrs' is a function which is passed the current attrs and state of the widget,
* and returns a hash of values to pass to attach
*
**/
addHeaderPanel(name, toggle, transformAttrs) {
attachAdditionalPanel(name, toggle, transformAttrs);
}
/**
* Adds a pluralization to the store
*

View File

@ -5,6 +5,7 @@ import { SEPARATOR } from 'discourse/lib/category-hashtags';
import Category from 'discourse/models/category';
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
import userSearch from 'discourse/lib/user-search';
import { userPath } from 'discourse/lib/url';
export function translateResults(results, opts) {
@ -29,7 +30,7 @@ export function translateResults(results, opts) {
results.posts = results.posts.map(post => {
if (post.username) {
post.userPath = Discourse.getURL(`/users/${post.username.toLowerCase()}`);
post.userPath = userPath(post.username.toLowerCase());
}
post = Post.create(post);
post.set('topic', topicMap[post.topic_id]);

View File

@ -11,17 +11,23 @@ export default function(name, opts) {
const controllerName = opts.admin ? `modals/${name}` : name;
const controller = container.lookup('controller:' + controllerName);
let controller = container.lookup('controller:' + controllerName);
const templateName = opts.templateName || Ember.String.dasherize(name);
const renderArgs = { into: 'modal', outlet: 'modalBody'};
if (controller) { renderArgs.controller = controllerName; }
if (controller) {
renderArgs.controller = controllerName;
} else {
// use a basic controller
renderArgs.controller = 'basic-modal-body';
controller = container.lookup(`controller:${renderArgs.controller}`);
}
if (opts.addModalBodyView) {
renderArgs.view = 'modal-body';
}
const modalName = `modal/${templateName}`;
const fullName = opts.admin ? `admin/templates/${modalName}` : modalName;
route.render(fullName, renderArgs);
@ -29,13 +35,11 @@ export default function(name, opts) {
modalController.set('title', I18n.t(opts.title));
}
if (controller) {
controller.set('modal', modalController);
const model = opts.model;
if (model) { controller.set('model', model); }
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);
}
controller.set('modal', modalController);
const model = opts.model;
if (model) { controller.set('model', model); }
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);
return controller;
};

View File

@ -1,3 +1,5 @@
import { userPath } from 'discourse/lib/url';
function actionDescription(action, acted, count) {
if (acted) {
if (count <= 1) {
@ -39,7 +41,7 @@ export function transformBasicPost(post) {
via_email: post.via_email,
isAutoGenerated: post.is_auto_generated,
user_id: post.user_id,
usernameUrl: Discourse.getURL(`/users/${post.username}`),
usernameUrl: userPath(post.username),
username: post.username,
avatar_template: post.avatar_template,
bookmarked: post.bookmarked,

View File

@ -18,6 +18,27 @@ const SERVER_SIDE_ONLY = [
/\.json$/,
];
export function rewritePath(path) {
const params = path.split("?");
let result = params[0];
rewrites.forEach(rw => result = result.replace(rw.regexp, rw.replacement));
if (params.length > 1) {
result += `?${params[1]}`;
}
return result;
}
export function clearRewrites() {
rewrites.length = 0;
}
export function userPath(subPath) {
return Discourse.getURL(subPath ? `/u/${subPath}` : '/u');
}
let _jumpScheduled = false;
export function jumpToElement(elementId) {
if (_jumpScheduled || Ember.isEmpty(elementId)) { return; }
@ -47,8 +68,8 @@ const DiscourseURL = Ember.Object.extend({
opts = opts || {};
const holderId = `#post_${postNumber}`;
_transitioning = true;
Em.run.schedule('afterRender', () => {
_transitioning = postNumber > 1;
Ember.run.schedule('afterRender', () => {
let elementId;
let holder;
@ -87,6 +108,10 @@ const DiscourseURL = Ember.Object.extend({
}
lockon.lock();
if (lockon.elementTop() < 1) {
_transitioning = false;
return;
}
});
},
@ -95,7 +120,6 @@ const DiscourseURL = Ember.Object.extend({
if (window.history &&
window.history.pushState &&
window.history.replaceState &&
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) &&
(window.location.pathname !== path)) {
// Always use replaceState in the next runloop to prevent weird routes changing
@ -173,15 +197,14 @@ const DiscourseURL = Ember.Object.extend({
if (path.indexOf('/my/') === 0) {
const currentUser = Discourse.User.current();
if (currentUser) {
path = path.replace('/my/', '/users/' + currentUser.get('username_lower') + "/");
path = path.replace('/my/', userPath(currentUser.get('username_lower') + "/"));
} else {
document.location.href = "/404";
return;
}
}
rewrites.forEach(rw => path = path.replace(rw.regexp, rw.replacement));
path = rewritePath(path);
if (this.navigatedToPost(oldPath, path, opts)) { return; }
if (oldPath === path) {

View File

@ -1,4 +1,5 @@
import { CANCELLED_STATUS } from 'discourse/lib/autocomplete';
import { userPath } from 'discourse/lib/url';
var cache = {},
cacheTopicId,
@ -14,7 +15,7 @@ function performSearch(term, topicId, includeGroups, includeMentionableGroups, a
}
// need to be able to cancel this
oldSearch = $.ajax(Discourse.getURL('/users/search/users'), {
oldSearch = $.ajax(userPath('search/users'), {
data: { term: term,
topic_id: topicId,
include_groups: includeGroups,

View File

@ -65,10 +65,6 @@ export function postUrl(slug, topicId, postNumber) {
return url;
}
export function userUrl(username) {
return Discourse.getURL("/users/" + username.toLowerCase());
}
export function emailValid(email) {
// see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
const re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;

View File

@ -1,4 +1,6 @@
import { defaultHomepage } from 'discourse/lib/utilities';
import { rewritePath } from 'discourse/lib/url';
const rootURL = Discourse.BaseUri;
const BareRouter = Ember.Router.extend({
@ -6,6 +8,7 @@ const BareRouter = Ember.Router.extend({
location: Ember.testing ? 'none': 'discourse-location',
handleURL(url) {
url = rewritePath(url);
const params = url.split('?');
if (params[0] === "/") {

View File

@ -1,5 +1,17 @@
import showModal from 'discourse/lib/show-modal';
export default Ember.Mixin.create({
flash(text, messageClass) {
this.appEvents.trigger('modal-body:flash', { text, messageClass });
},
showModal(...args) {
return showModal(...args);
},
actions: {
closeModal() {
this.get('modal').send('closeModal');
}
}
});

View File

@ -32,7 +32,7 @@ export default Ember.Mixin.create({
topicTitle,
topicBody,
archetypeId: 'private_message',
draftKey: 'new_private_message'
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY
});
}

View File

@ -15,6 +15,7 @@ const CLOSED = 'closed',
// The actions the composer can take
CREATE_TOPIC = 'createTopic',
PRIVATE_MESSAGE = 'privateMessage',
NEW_PRIVATE_MESSAGE_KEY = 'new_private_message',
REPLY = 'reply',
EDIT = 'edit',
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
@ -815,6 +816,7 @@ Composer.reopenClass({
EDIT,
// Draft key
NEW_PRIVATE_MESSAGE_KEY,
REPLY_AS_NEW_TOPIC_KEY,
REPLY_AS_NEW_PRIVATE_MESSAGE_KEY
});

View File

@ -1,5 +1,6 @@
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { userPath } from 'discourse/lib/url';
const Invite = Discourse.Model.extend({
@ -41,7 +42,7 @@ Invite.reopenClass({
if (!Em.isNone(search)) { data.search = search; }
data.offset = offset || 0;
return ajax("/users/" + user.get('username_lower') + "/invited.json", {data}).then(function (result) {
return ajax(userPath(user.get('username_lower') + "/invited.json"), {data}).then(function (result) {
result.invites = result.invites.map(function (i) {
return Invite.create(i);
});
@ -52,7 +53,7 @@ Invite.reopenClass({
findInvitedCount(user) {
if (!user) { return Em.RSVP.resolve(); }
return ajax("/users/" + user.get('username_lower') + "/invited_count.json").then(result => Em.Object.create(result.counts));
return ajax(userPath(user.get('username_lower') + "/invited_count.json")).then(result => Em.Object.create(result.counts));
},
reinviteAll() {

View File

@ -15,6 +15,7 @@ export default RestModel.extend({
loadingAbove: null,
loadingBelow: null,
loadingFilter: null,
loadingNearPost: null,
stagingPost: null,
postsWithPlaceholders: null,
timelineLookup: null,
@ -206,6 +207,7 @@ export default RestModel.extend({
// TODO: if we have all the posts in the filter, don't go to the server for them.
this.set('loadingFilter', true);
this.set('loadingNearPost', opts.nearPost);
opts = _.merge(opts, this.get('streamFilters'));
@ -216,6 +218,8 @@ export default RestModel.extend({
}).catch(result => {
this.errorLoading(result);
throw result;
}).finally(() => {
this.set('loadingNearPost', null);
});
},
@ -540,7 +544,9 @@ export default RestModel.extend({
return Ember.RSVP.Promise.resolve();
},
triggerChangedPost(postId, updatedAt) {
triggerChangedPost(postId, updatedAt, opts) {
opts = opts || {};
const resolved = Ember.RSVP.Promise.resolve();
if (!postId) { return resolved; }
@ -548,7 +554,13 @@ export default RestModel.extend({
if (existing && existing.updated_at !== updatedAt) {
const url = "/posts/" + postId;
const store = this.store;
return ajax(url).then(p => this.storePost(store.createRecord('post', p)));
return ajax(url).then(p => {
if (opts.preserveCooked) {
p.cooked = existing.get('cooked');
}
this.storePost(store.createRecord('post', p));
});
}
return resolved;
},

View File

@ -2,11 +2,12 @@ import { ajax } from 'discourse/lib/ajax';
import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import ActionSummary from 'discourse/models/action-summary';
import { url, propertyEqual } from 'discourse/lib/computed';
import { propertyEqual } from 'discourse/lib/computed';
import Quote from 'discourse/lib/quote';
import computed from 'ember-addons/ember-computed-decorators';
import { postUrl } from 'discourse/lib/utilities';
import { cook } from 'discourse/lib/text';
import { userPath } from 'discourse/lib/url';
const Post = RestModel.extend({
@ -60,7 +61,10 @@ const Post = RestModel.extend({
return postNumber === 1 ? baseUrl + "/1" : baseUrl;
},
usernameUrl: url('username', '/users/%@'),
@computed('username')
usernameUrl(username) {
return userPath(username);
},
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),

View File

@ -0,0 +1,25 @@
import { ajax } from 'discourse/lib/ajax';
import RestModel from 'discourse/models/rest';
const TopicStatusUpdate = RestModel.extend({});
TopicStatusUpdate.reopenClass({
updateStatus(topicId, time, basedOnLastPost, statusType, categoryId) {
let data = {
time: time,
timezone_offset: (new Date().getTimezoneOffset()),
status_type: statusType
};
if (basedOnLastPost) data.based_on_last_post = basedOnLastPost;
if (categoryId) data.category_id = categoryId;
return ajax({
url: `/t/${topicId}/status_update`,
type: 'POST',
data
});
}
});
export default TopicStatusUpdate;

View File

@ -9,6 +9,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import { censor } from 'pretty-text/censored-words';
import { emojiUnescape } from 'discourse/lib/text';
import PreloadStore from 'preload-store';
import { userPath } from 'discourse/lib/url';
export function loadTopicView(topic, args) {
const topicId = topic.get('id');
@ -182,9 +183,10 @@ const Topic = RestModel.extend({
return this.urlForPostNumber(1) + (this.get('has_summary') ? "?filter=summary" : "");
}.property('url'),
lastPosterUrl: function() {
return Discourse.getURL("/users/") + this.get("last_poster.username");
}.property('last_poster'),
@computed('last_poster.username')
lastPosterUrl(username) {
return userPath(username);
},
// The amount of new posts to display. It might be different than what the server
// tells us if we are still asynchronously flushing our "recently read" data.
@ -221,16 +223,12 @@ const Topic = RestModel.extend({
toggleStatus(property) {
this.toggleProperty(property);
this.saveStatus(property, !!this.get(property));
return this.saveStatus(property, !!this.get(property));
},
saveStatus(property, value, until) {
if (property === 'closed') {
this.incrementProperty('posts_count');
if (value === true) {
this.set('details.auto_close_at', null);
}
}
return ajax(this.get('url') + "/status", {
type: 'PUT',
@ -378,9 +376,8 @@ const Topic = RestModel.extend({
},
reload() {
const self = this;
return ajax('/t/' + this.get('id'), { type: 'GET' }).then(function(topic_json) {
self.updateFromJson(topic_json);
return ajax(`/t/${this.get('id')}`, { type: 'GET' }).then(topic_json => {
this.updateFromJson(topic_json);
});
},

View File

@ -1,9 +1,9 @@
import RestModel from 'discourse/models/rest';
import { url } from 'discourse/lib/computed';
import { on } from 'ember-addons/ember-computed-decorators';
import computed from 'ember-addons/ember-computed-decorators';
import UserActionGroup from 'discourse/models/user-action-group';
import { postUrl } from 'discourse/lib/utilities';
import { userPath } from 'discourse/lib/url';
const UserActionTypes = {
likes_given: 1,
@ -79,14 +79,21 @@ const UserAction = RestModel.extend({
presentName: Ember.computed.or('name', 'username'),
targetDisplayName: Ember.computed.or('target_name', 'target_username'),
actingDisplayName: Ember.computed.or('acting_name', 'acting_username'),
targetUserUrl: url('target_username', '/users/%@'),
@computed('target_username')
targetUserUrl(username) {
return userPath(username);
},
@computed("username")
usernameLower(username) {
return username.toLowerCase();
},
userUrl: url('usernameLower', '/users/%@'),
@computed('usernameLower')
userUrl(usernameLower) {
return userPath(usernameLower);
},
@computed()
postUrl() {

View File

@ -15,6 +15,7 @@ import Topic from 'discourse/models/topic';
import { emojiUnescape } from 'discourse/lib/text';
import PreloadStore from 'preload-store';
import { defaultHomepage } from 'discourse/lib/utilities';
import { userPath } from 'discourse/lib/url';
const User = RestModel.extend({
@ -71,7 +72,7 @@ const User = RestModel.extend({
@computed()
path() {
// no need to observe, requires a hard refresh to update
return Discourse.getURL(`/users/${this.get('username_lower')}`);
return userPath(this.get('username_lower'));
},
@computed()
@ -124,11 +125,10 @@ const User = RestModel.extend({
// directly targetted so go to inbox
if (!groups || (allowedUsers && allowedUsers.findBy("id", userId))) {
return Discourse.getURL(`/users/${username}/messages`);
return userPath(`${username}/messages`);
} else {
if (groups && groups[0])
{
return Discourse.getURL(`/users/${username}/messages/group/${groups[0].name}`);
if (groups && groups[0]) {
return userPath(`${username}/messages/group/${groups[0].name}`);
}
}
@ -146,6 +146,11 @@ const User = RestModel.extend({
return defaultHomepage() === "latest" ? Discourse.getURL('/?state=watching') : Discourse.getURL('/latest?state=watching');
},
@computed()
trackingTopicsPath() {
return defaultHomepage() === "latest" ? Discourse.getURL('/?state=tracking') : Discourse.getURL('/latest?state=tracking');
},
@computed("username")
username_lower(username) {
return username.toLowerCase();
@ -179,14 +184,14 @@ const User = RestModel.extend({
},
changeUsername(new_username) {
return ajax(`/users/${this.get('username_lower')}/preferences/username`, {
return ajax(userPath(`${this.get('username_lower')}/preferences/username`), {
type: 'PUT',
data: { new_username }
});
},
changeEmail(email) {
return ajax(`/users/${this.get('username_lower')}/preferences/email`, {
return ajax(userPath(`${this.get('username_lower')}/preferences/email`), {
type: 'PUT',
data: { email }
});
@ -254,7 +259,7 @@ const User = RestModel.extend({
// TODO: We can remove this when migrated fully to rest model.
this.set('isSaving', true);
return ajax(`/users/${this.get('username_lower')}.json`, {
return ajax(userPath(`${this.get('username_lower')}.json`), {
data: data,
type: 'PUT'
}).then(result => {
@ -330,7 +335,7 @@ const User = RestModel.extend({
const user = this;
return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => {
return ajax(`/users/${user.get('username')}.json`, { data: options });
return ajax(userPath(`${user.get('username')}.json`), { data: options });
}).then(json => {
if (!Em.isEmpty(json.user.stats)) {
@ -375,13 +380,13 @@ const User = RestModel.extend({
findStaffInfo() {
if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); }
return ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => {
return ajax(userPath(`${this.get("username_lower")}/staff-info.json`)).then(info => {
this.setProperties(info);
});
},
pickAvatar(upload_id, type, avatar_template) {
return ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
return ajax(userPath(`${this.get("username_lower")}/preferences/avatar/pick`), {
type: 'PUT',
data: { upload_id, type }
}).then(() => this.setProperties({
@ -437,7 +442,7 @@ const User = RestModel.extend({
"delete": function() {
if (this.get('can_delete_account')) {
return ajax("/users/" + this.get('username'), {
return ajax(userPath(this.get('username')), {
type: 'DELETE',
data: {context: window.location.pathname}
});
@ -448,14 +453,14 @@ const User = RestModel.extend({
dismissBanner(bannerKey) {
this.set("dismissed_banner_key", bannerKey);
ajax(`/users/${this.get('username')}`, {
ajax(userPath(this.get('username')), {
type: 'PUT',
data: { dismissed_banner_key: bannerKey }
});
},
checkEmail() {
return ajax(`/users/${this.get("username_lower")}/emails.json`, {
return ajax(userPath(`${this.get("username_lower")}/emails.json`), {
data: { context: window.location.pathname }
}).then(result => {
if (result) {
@ -468,7 +473,7 @@ const User = RestModel.extend({
},
summary() {
return ajax(`/users/${this.get("username_lower")}/summary.json`)
return ajax(userPath(`${this.get("username_lower")}/summary.json`))
.then(json => {
const summary = json["user_summary"];
const topicMap = {};
@ -526,7 +531,7 @@ User.reopenClass(Singleton, {
},
checkUsername(username, email, for_user_id) {
return ajax('/users/check_username', {
return ajax(userPath('check_username'), {
data: { username, email, for_user_id }
});
},
@ -557,7 +562,7 @@ User.reopenClass(Singleton, {
},
createAccount(attrs) {
return ajax("/users", {
return ajax(userPath(), {
data: {
name: attrs.accountName,
email: attrs.accountEmail,

View File

@ -62,9 +62,9 @@ export default function() {
});
// User routes
this.route('users', { resetNamespace: true });
this.route('password-reset', { path: '/users/password-reset/:token' });
this.route('user', { path: '/users/:username', resetNamespace: true }, function() {
this.route('users', { resetNamespace: true, path: '/u' });
this.route('password-reset', { path: '/u/password-reset/:token' });
this.route('user', { path: '/u/:username', resetNamespace: true }, function() {
this.route('summary');
this.route('userActivity', { path: '/activity', resetNamespace: true }, function() {
this.route('topics');

View File

@ -7,6 +7,7 @@ import Category from 'discourse/models/category';
import mobile from 'discourse/lib/mobile';
import { findAll } from 'discourse/models/login-method';
import { getOwner } from 'discourse-common/lib/get-owner';
import { userPath } from 'discourse/lib/url';
function unlessReadOnly(method, message) {
return function() {
@ -23,7 +24,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
actions: {
toggleAnonymous() {
ajax("/users/toggle-anon", {method: 'POST'}).then(() => {
ajax(userPath("toggle-anon"), {method: 'POST'}).then(() => {
window.location.reload();
});
},

View File

@ -1,5 +1,6 @@
import PreloadStore from 'preload-store';
import { ajax } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
export default Discourse.Route.extend({
titleToken() {
@ -15,7 +16,7 @@ export default Discourse.Route.extend({
afterModel(model) {
// confirm token here so email clients who crawl URLs don't invalidate the link
if (model) {
return ajax({ url: `/users/confirm-email-token/${model.token}.json`, dataType: 'json' });
return ajax({ url: userPath(`confirm-email-token/${model.token}.json`), dataType: 'json' });
}
}
});

View File

@ -50,9 +50,11 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('flag').setProperties({ selected: null, flagTopic: true });
},
showAutoClose() {
showModal('edit-topic-auto-close', { model: this.modelFor('topic') });
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
showTopicStatusUpdate() {
const model = this.modelFor('topic');
model.set('topic_status_update', Ember.Object.create(model.get('topic_status_update')));
showModal('edit-topic-status-update', { model });
this.controllerFor('modal').set('modalClass', 'topic-close-modal');
},
showChangeTimestamp() {

View File

@ -83,14 +83,14 @@ export default Discourse.Route.extend({
activate() {
this._super();
const user = this.modelFor('user');
this.messageBus.subscribe("/users/" + user.get('username_lower'), function(data) {
this.messageBus.subscribe("/u/" + user.get('username_lower'), function(data) {
user.loadUserAction(data);
});
},
deactivate() {
this._super();
this.messageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));
this.messageBus.unsubscribe("/u/" + this.modelFor('user').get('username_lower'));
// Remove the search context
this.searchService.set('searchContext', null);

View File

@ -1,19 +0,0 @@
<div class="auto-close-fields">
<div>
<label>
{{fa-icon "clock-o"}}
{{i18n 'composer.auto_close.label'}}
{{text-field value=autoCloseTime}}
{{autoCloseUnits}}
</label>
</div>
<div class="examples">
{{autoCloseExamples}}
</div>
<div>
<label>
{{input type="checkbox" checked=autoCloseBasedOnLastPost}}
{{i18n 'composer.auto_close.based_on_last_post'}}
</label>
</div>
</div>

View File

@ -0,0 +1,24 @@
<div class="auto-update-input">
<div class="control-group">
<label>
{{i18n inputLabelKey}}
{{text-field value=input}}
{{i18n inputUnitsKey}}
</label>
{{#if inputExamplesKey}}
<div class="examples">
{{i18n inputExamplesKey}}
</div>
{{/if}}
</div>
{{#unless hideBasedOnLastPost}}
<div class="control-group">
<label>
{{input type="checkbox" checked=basedOnLastPost}}
{{i18n 'topic.auto_close.based_on_last_post'}}
</label>
</div>
{{/unless}}
</div>

View File

@ -1,8 +1,10 @@
<section class='field'>
{{auto-close-form autoCloseTime=category.auto_close_hours
autoCloseBasedOnLastPost=category.auto_close_based_on_last_post
autoCloseExamples=""
limited="true" }}
{{auto-update-input
inputLabelKey='topic.auto_close.label'
input=category.auto_close_hours
basedOnLastPost=category.auto_close_based_on_last_post
inputExamplesKey=''
limited=true}}
</section>
<section class='field'>

View File

@ -0,0 +1,3 @@
<div class="modal-footer">
<button class='btn btn-primary' {{action closeModal}}>{{i18n 'close'}}</button>
</div>

View File

@ -8,7 +8,7 @@
toggleClosed=toggleClosed
toggleArchived=toggleArchived
toggleVisibility=toggleVisibility
showAutoClose=showAutoClose
showTopicStatusUpdate=showTopicStatusUpdate
showFeatureTopic=showFeatureTopic
showChangeTimestamp=showChangeTimestamp
convertToPublicTopic=convertToPublicTopic

View File

@ -35,7 +35,7 @@
<button title="{{i18n 'topics.bulk.dismiss_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissReadPosts"}}>{{i18n 'topics.bulk.dismiss_button'}}</button>
{{/if}}
{{#if showResetNew}}
<button id='dismiss-new' class='btn dismiss-read' {{action "resetNew"}}>{{i18n 'topics.bulk.dismiss_new'}}</button>
<button id='dismiss-new' class='btn dismiss-read' {{action "resetNew"}}><i class="fa fa-check"></i> {{i18n 'topics.bulk.dismiss_new'}}</button>
{{/if}}
{{#if latest}}

View File

@ -3,10 +3,13 @@
<div class="modal-middle-container">
<div class="modal-inner-container">
<div class="modal-header">
<a class="close" {{action "closeModal"}}>{{fa-icon "times"}}</a>
<h3>{{title}}</h3>
<div class="clearfix"></div>
<div class='modal-close'>
<a class="close" {{action "closeModal"}}>{{fa-icon "times"}}</a>
</div>
</div>
<div id='modal-alert'></div>
{{outlet "modalBody"}}
{{#each errors as |error|}}

View File

@ -0,0 +1,12 @@
{{#d-modal-body}}
<p>{{i18n "login.provide_new_email"}}</p>
{{input value=newEmail class="activate-new-email"}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action="changeEmail"
label="login.submit_new_email"
disabled=submitDisabled
class="btn-primary"}}
{{d-button action="closeModal" label="close"}}
</div>

View File

@ -0,0 +1,5 @@
{{#d-modal-body}}
{{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}}
{{/d-modal-body}}
{{modal-footer-close closeModal=(action "closeModal")}}

View File

@ -1,20 +0,0 @@
<form>
{{#d-modal-body title="topic.auto_close_title" autoFocus="false"}}
{{auto-close-form autoCloseTime=model.auto_close_time
autoCloseValid=auto_close_valid
autoCloseBasedOnLastPost=model.details.auto_close_based_on_last_post
limited=model.details.auto_close_based_on_last_post }}
{{#if willCloseImmediately}}
<div class="warning">
{{fa-icon "warning"}}
{{willCloseI18n}}
</div>
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary" disabled=disable_submit label="topic.auto_close_save" action="saveAutoClose"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
{{conditional-loading-spinner size="small" condition=loading}}
{{d-button class="pull-right" action="removeAutoClose" label="topic.auto_close_remove"}}
</div>
</form>

View File

@ -0,0 +1,103 @@
<form>
{{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}}
<div class="radios">
{{radio-button
disabled=disableAutoClose
name="auto-close"
id="auto-close"
value=closeStatusType
selection=selection}}
<label class="radio" for="auto-close">
{{fa-icon "clock-o"}} {{fa-icon "lock"}}
{{#if model.closed}}
{{i18n 'topic.temp_open.title'}}
{{else}}
{{i18n 'topic.auto_close.title'}}
{{/if}}
</label>
{{radio-button
disabled=disableAutoOpen
name="auto-reopen"
id="auto-reopen"
value=openStatusType
selection=selection}}
<label class="radio" for="auto-reopen">
{{fa-icon "clock-o"}} {{fa-icon "unlock"}}
{{#if model.closed}}
{{i18n 'topic.auto_reopen.title'}}
{{else}}
{{i18n 'topic.temp_close.title'}}
{{/if}}
</label>
{{radio-button
disabled=disablePublishToCategory
name="publish-to-category"
id="publish-to-category"
value=publishToCategoryStatusType
selection=selection}}
<label class="radio" for="publish-to-category">
{{fa-icon "clock-o"}} {{i18n 'topic.publish_to_category.title'}}
</label>
</div>
<div>
{{#if autoOpen}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
hideBasedOnLastPost=true
basedOnLastPost=false}}
{{else if publishToCategory}}
<div class="control-group">
<label>{{i18n 'topic.topic_status_update.publish_to'}}</label>
{{category-chooser valueAttribute="id" value=categoryId excludeCategoryId=excludeCategoryId}}
</div>
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
hideBasedOnLastPost=true
basedOnLastPost=false}}
{{else if autoClose}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
limited=topicStatusUpdate.based_on_last_post
basedOnLastPost=topicStatusUpdate.based_on_last_post}}
{{#if willCloseImmediately}}
<div class="warning">
{{fa-icon "warning"}}
{{willCloseI18n}}
</div>
{{/if}}
{{/if}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary"
disabled=saveDisabled
label="topic.topic_status_update.save"
action="saveStatusUpdate"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
{{conditional-loading-spinner size="small" condition=loading}}
{{#if topicStatusUpdate.execute_at}}
{{d-button class="pull-right btn-danger"
action="removeStatusUpdate"
label='topic.topic_status_update.remove'}}
{{/if}}
</div>
</form>

View File

@ -1,12 +1,14 @@
{{#d-modal-body}}
{{#if emailSent}}
{{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}}
{{else}}
{{{i18n 'login.not_activated' sentTo=sentTo}}}
<a href {{action "sendActivationEmail"}} class="resend-link">{{i18n 'login.resend_activation_email'}}</a>
{{/if}}
{{{i18n 'login.not_activated' sentTo=sentTo}}}
{{/d-modal-body}}
<div class="modal-footer">
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
{{d-button action="sendActivationEmail"
label="login.resend_title"
icon="envelope"
class="btn-primary resend"}}
{{d-button action="editActivationEmail"
label="login.change_email"
icon="pencil"
class="edit-email"}}
</div>

View File

@ -263,12 +263,14 @@
<div class="controls category-controls">
<a href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.watched_topics_link'}}</a>
</div>
<div class="instructions"></div>
<div class="controls category-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_categories'}}</label>
{{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
<div class="controls category-controls">
<a href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
</div>
<div class="controls category-controls">
<label><span class="icon fa fa-dot-circle-o watching-first-post"></span> {{i18n 'user.watched_first_post_categories'}}</label>
{{category-selector categories=model.watchedFirstPostCategories}}

View File

@ -81,7 +81,7 @@
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showAutoClose=(action "topicRouteAction" "showAutoClose")
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
convertToPublicTopic=(action "convertToPublicTopic")
@ -107,7 +107,7 @@
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showAutoClose=(action "topicRouteAction" "showAutoClose")
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
convertToPublicTopic=(action "convertToPublicTopic")
@ -174,7 +174,7 @@
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
{{#if loadedAllPosts}}
{{topic-closing topic=model}}
{{topic-status-info topic=model}}
{{#if session.showSignupCta}}
{{! replace "Log In to Reply" with the infobox }}
{{signup-cta}}
@ -188,7 +188,7 @@
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showAutoClose=(action "topicRouteAction" "showAutoClose")
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
convertToPublicTopic=(action "convertToPublicTopic")

View File

@ -17,6 +17,7 @@
{{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}}
{{#if canBulkInvite}}
{{csv-uploader uploading=uploading}}
<a href="https://meta.discourse.org/t/sending-bulk-user-invites/16468" target="_blank" style="color:black;">{{fa-icon "question-circle"}}</a>
{{/if}}
{{#if showReinviteAllButton}}
{{#if reinvitedAll}}

View File

@ -3,12 +3,13 @@ import { avatarFor } from 'discourse/widgets/post';
import { iconNode } from 'discourse/helpers/fa-icon-node';
import { h } from 'virtual-dom';
import { dateNode } from 'discourse/helpers/node';
import { userPath } from 'discourse/lib/url';
export function avatarAtts(user) {
return { template: user.avatar_template,
username: user.username,
post_url: user.post_url,
url: Discourse.getURL('/users/') + user.username_lower };
url: userPath(user.username_lower) };
}
createWidget('small-user-list', {

View File

@ -2,6 +2,7 @@ import { createWidget, applyDecorators } from 'discourse/widgets/widget';
import { h } from 'virtual-dom';
import DiscourseURL from 'discourse/lib/url';
import { ajax } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
const flatten = array => [].concat.apply([], array);
@ -19,7 +20,7 @@ createWidget('priority-faq-link', {
click(e) {
e.preventDefault();
if (this.siteSettings.faq_url === this.attrs.href) {
ajax("/users/read-faq", { method: "POST" }).then(() => {
ajax(userPath("read-faq"), { method: "POST" }).then(() => {
this.currentUser.set('read_faq', true);
DiscourseURL.routeToTag($(e.target).closest('a')[0]);
});

View File

@ -166,6 +166,11 @@ createWidget('header-buttons', {
const forceContextEnabled = ['category', 'user', 'private_messages'];
let additionalPanels = [];
export function attachAdditionalPanel(name, toggle, transformAttrs) {
additionalPanels.push({ name, toggle, transformAttrs });
}
export default createWidget('header', {
tagName: 'header.d-header.clearfix',
buildKey: () => `header`,
@ -214,6 +219,12 @@ export default createWidget('header', {
panels.push(this.attach('user-menu'));
}
additionalPanels.map((panel) => {
if (this.state[panel.toggle]) {
panels.push(this.attach(panel.name, panel.transformAttrs.call(this, attrs, state)));
}
});
const contents = [ this.attach('home-logo', { minimized: !!attrs.topic }),
h('div.panel.clearfix', panels) ];

View File

@ -6,6 +6,7 @@ import { h } from 'virtual-dom';
import { emojiUnescape } from 'discourse/lib/text';
import { postUrl, escapeExpression } from 'discourse/lib/utilities';
import { setTransientHeader } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
const LIKED_TYPE = 5;
const INVITED_TYPE = 8;
@ -45,11 +46,11 @@ createWidget('notification-item', {
}
if (attrs.notification_type === INVITED_TYPE) {
return Discourse.getURL('/users/' + data.display_username);
return userPath(data.display_username);
}
if (data.group_id) {
return Discourse.getURL('/users/' + data.username + '/messages/group/' + data.group_name);
return userPath(data.username + '/messages/group/' + data.group_name);
}
},

View File

@ -122,12 +122,13 @@ export default createWidget('topic-admin-menu', {
action: 'toggleClosed',
icon: 'lock',
label: 'actions.close' });
buttons.push({ className: 'topic-admin-autoclose',
action: 'showAutoClose',
icon: 'clock-o',
label: 'actions.auto_close' });
}
buttons.push({ className: 'topic-admin-status-update',
action: 'showTopicStatusUpdate',
icon: 'clock-o',
label: 'actions.timed_update' });
const isPrivateMessage = topic.get('isPrivateMessage');
if (!isPrivateMessage && topic.get('visible')) {

View File

@ -159,8 +159,7 @@ createWidget('topic-map-expanded', {
if (l.title && l.title.length) {
const domain = l.domain;
if (domain && domain.length) {
const s = domain.split('.');
host = h('span.domain', s[s.length-2] + "." + s[s.length-1]);
host = h('span.domain', domain);
}
}

View File

@ -154,8 +154,7 @@ createWidget('timeline-scrollarea', {
if (this.state.position !== result.current) {
this.state.position = result.current;
const timeline = this._findAncestorWithProperty('updatePosition');
timeline.updatePosition.call(timeline, result.current);
this.sendWidgetAction('updatePosition', result.current);
}
return result;
@ -287,8 +286,6 @@ export default createWidget('topic-timeline', {
this.state.position = pos;
this.state.excerpt = "";
this.scheduleRerender();
const stream = this.attrs.topic.get('postStream');
// a little debounce to avoid flashing
@ -300,7 +297,6 @@ export default createWidget('topic-timeline', {
// we have an off by one, stream is zero based,
// pos is 1 based
stream.excerpt(pos-1).then(info => {
if (info && this.state.position === pos) {
let excerpt = "";
@ -308,9 +304,7 @@ export default createWidget('topic-timeline', {
excerpt = "<span class='username'>" + info.username + ":</span> ";
}
excerpt += info.excerpt;
this.state.excerpt = excerpt;
this.state.excerpt = excerpt + info.excerpt;
this.scheduleRerender();
}
});
@ -332,22 +326,20 @@ export default createWidget('topic-timeline', {
}
let elems = [h('h2', this.attach('link', {
contents: ()=>titleHTML,
className: 'fancy-title',
action: 'jumpTop'}))];
contents: () => titleHTML,
className: 'fancy-title',
action: 'jumpTop'
}))];
if (this.state.excerpt) {
elems.push(
new RawHtml({
html: "<div class='post-excerpt'>" + this.state.excerpt + "</div>"
}));
elems.push(new RawHtml({
html: "<div class='post-excerpt'>" + this.state.excerpt + "</div>"
}));
}
result.push(h('div.title', elems));
}
if (!attrs.fullScreen && currentUser && currentUser.get('canManageTopic')) {
result.push(h('div.timeline-controls', this.attach('topic-admin-menu-button', { topic })));
}

View File

@ -9,6 +9,8 @@ export function register(helper, codeName, args, emitter) {
start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"),
emitter(blockContents, matches) {
const options = helper.getOptions();
while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) {
blockContents[0] = String(blockContents[0]).replace(/^\s+/, '');
@ -22,7 +24,11 @@ export function register(helper, codeName, args, emitter) {
let contents = [];
if (blockContents.length) {
const nextContents = blockContents.slice(1);
blockContents = this.processBlock(blockContents[0], nextContents).concat(nextContents);
blockContents = this.processBlock(blockContents[0], nextContents);
nextContents.forEach(nc => {
blockContents = blockContents.concat(this.processBlock(nc, []));
});
blockContents.forEach(bc => {
if (typeof bc === "string" || bc instanceof String) {

View File

@ -38,7 +38,7 @@ export function setup(helper) {
const type = mentionLookup && mentionLookup(name);
if (type === "user") {
return ['a', {'class': 'mention', href: opts.getURL("/users/") + name.toLowerCase()}, mention];
return ['a', {'class': 'mention', href: opts.getURL("/u/") + name.toLowerCase()}, mention];
} else if (type === "group") {
return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention];
} else {

View File

@ -10,6 +10,7 @@ registerOption((siteSettings, opts) => {
export function setup(helper) {
register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
const params = {'class': 'quote'};
let username = null;
const opts = helper.getOptions();

View File

@ -133,16 +133,7 @@ div.ac-wrap {
}
}
.auto-close-fields {
div:not(:first-child) {
margin-top: 10px;
}
label {
font-size: 1em;
}
input {
width: 150px;
}
.auto-update-input {
.examples {
color: lighten($primary, 40%);
}
@ -187,5 +178,3 @@ div.ac-wrap {
.cooked > *:first-child {
margin-top: 0;
}

View File

@ -295,34 +295,13 @@
}
}
.edit-auto-close-modal {
.btn.pull-right {
margin-right: 10px;
}
form {
margin: 0;
}
.auto-close-fields {
i.fa-clock-o {
font-size: 1.143em;
}
input {
margin: 0;
}
}
}
.edit-category-modal {
.auto-close-fields, .num-featured-topics-fields, .position-fields {
.auto-update-input, .num-featured-topics-fields, .position-fields {
input[type=text] {
width: 50px;
}
}
.auto-close-fields label {
font-size: .929em;
}
.subcategory-list-style-field {
margin-left: 16px;
}
@ -398,3 +377,10 @@
}
}
.modal-button-bar {
margin-top: 1em;
button {
margin-right: 0.5em;
}
}

View File

@ -1,4 +1,4 @@
.fa.muted {
.fa.muted, .fa.watching-first-post {
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
}
.fa.tracking, .fa.watching {

View File

@ -93,6 +93,12 @@ $tag-color: scale-color($primary, $lightness: 40%);
}
}
.d-header .topic-header-extra {
display: inline-block;
.discourse-tags { display: inline-block; }
.topic-featured-link { margin-left: 8px; }
}
.select2-container-multi .select2-choices .select2-search-choice.discourse-tag-select2 {
padding-top: 5px;
-webkit-box-shadow: none;

View File

@ -0,0 +1,29 @@
.topic-close-modal {
label {
display: inline-block;
}
.radios {
padding-bottom: 20px;
display: inline-block;
input[type='radio'] {
vertical-align: middle;
margin: 0px;
}
label {
padding: 0 10px 0px 5px;
}
}
.btn.pull-right {
margin-right: 10px;
}
.auto-update-input {
input {
margin: 0;
}
}
}

View File

@ -297,7 +297,3 @@ and (max-width : 600px) {
border: none;
}
}
.user-preferences .watching-first-post.fa-dot-circle-o {
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
}

View File

@ -16,7 +16,7 @@
.show-topic-admin,
#topic-progress,
.quote-controls,
#topic-closing-info,
#topic-status-info,
div.lazyYT,
.post-info.edits,
.post-action,

View File

@ -342,7 +342,7 @@
display: block;
bottom: 8px;
}
.auto-close-fields .examples {
.auto-update-input .examples {
margin-top: 0;
padding-bottom: 8px;
}

View File

@ -46,15 +46,20 @@
margin-left: -1px;
}
.modal-close {
display: inline-block;
float: right;
margin: 7px;
}
.modal-header {
h3 {
display: inline-block;;
font-size: 1.429em;
padding: 10px 15px 7px;
}
.close {margin: 10px;}
}
.close {
float: right;
font-size: 1.429em;
text-decoration: none;
color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%));

View File

@ -79,7 +79,7 @@
}
}
#topic-closing-info {
#topic-status-info {
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%);
padding-top: 10px;
height: 20px;
@ -242,4 +242,3 @@ and (max-width : 485px) {
max-width: 100%;
}
}

View File

@ -49,23 +49,24 @@
margin-bottom: 5px;
}
.modal-close {
display: inline-block;
float: right;
}
.modal-header {
// we need tighter spacing on mobile for header
// this clearfix under the modal title h3 pushes it way down
.clearfix {
display:none;
}
padding: 10px 0px;
h3 {
display: inline-block;
font-size: 1.286em;
padding-left: 15px;
margin-bottom: 5px;
margin: 0px;
}
}
.close {
float: right;
font-size: 1.714em;
padding: 10px 15px 5px 5px;
margin: -15px 0 0 0;
color: $primary;
}

View File

@ -475,7 +475,9 @@ button.select-post {
.deleted {
background-color: dark-light-diff(rgba($danger,.7), $secondary, 50%, -60%);
.topic-body {
background-color: dark-light-diff(rgba($danger,.7), $secondary, 50%, -60%);
}
}
.deleted-user-avatar {

View File

@ -43,7 +43,7 @@
clear: both;
}
#topic-closing-info {
#topic-status-info {
margin-left: 10px;
}
@ -190,7 +190,7 @@ sup sup, sub sup, sup sub, sub sub { top: 0; }
}
// make mobile timeline top and bottom dates easier to select
.topic-timeline {
.topic-timeline {
.start-date { font-size: 110%; padding: 5px; }
.now-date { font-size: 110%; padding: 5px; }
}

View File

@ -69,6 +69,19 @@ class Admin::EmailController < Admin::AdminController
end
end
def smtp_should_reject
params.require(:from)
params.require(:to)
# These strings aren't localized; they are sent to an anonymous SMTP user.
if !User.exists?(email: Email.downcase(params[:from])) && !SiteSetting.enable_staged_users
render json: { reject: true, reason: "Mail from your address is not accepted. Do you have an account here?" }
elsif Email::Receiver.check_address(Email.downcase(params[:to])).nil?
render json: { reject: true, reason: "Mail to this address is not accepted. Check the address and try to send again?" }
else
render json: { reject: false }
end
end
def handle_mail
params.require(:email)
Email::Processor.process!(params[:email])

View File

@ -7,12 +7,9 @@ class Admin::ReportsController < Admin::AdminController
raise Discourse::NotFound unless report_type =~ /^[a-z0-9\_]+$/
start_date = 1.month.ago
start_date = Time.parse(params[:start_date]) if params[:start_date].present?
end_date = start_date + 1.month
end_date = Time.parse(params[:end_date]) if params[:end_date].present?
start_date = params[:start_date].present? ? Time.parse(params[:start_date]) : 30.days.ago
end_date = params[:end_date].present? ? Time.parse(params[:end_date]) : start_date + 30.days
if params.has_key?(:category_id) && params[:category_id].to_i > 0
category_id = params[:category_id].to_i
else

Some files were not shown because too many files have changed in this diff Show More