Version bump

This commit is contained in:
Neil Lalonde 2017-03-08 12:23:21 -05:00
commit 7d40cd92f8
385 changed files with 14020 additions and 5190 deletions

View File

@ -24,7 +24,7 @@ matrix:
fast_finish: true
rvm:
- 2.3.1
- 2.3.3
services:
- redis-server

View File

@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
lang_map = el_GR: el, es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml

View File

@ -92,7 +92,7 @@ gem 'pry-rails', require: false
gem 'r2', '~> 0.2.5', require: false
gem 'rake'
gem 'thor', require: false
gem 'rest-client'
gem 'rinku'
gem 'sanitize'

View File

@ -206,7 +206,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.1)
onebox (1.8.2)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3.4)
moneta (~> 0.8)
@ -470,6 +470,7 @@ DEPENDENCIES
sinatra
spork-rails
stackprof
thor
timecop
uglifier
unf

View File

@ -1,4 +1,3 @@
import DiscourseURL from 'discourse/lib/url';
import { ajax } from 'discourse/lib/ajax';
export default Ember.Controller.extend({
@ -39,7 +38,11 @@ export default Ember.Controller.extend({
},
download(backup) {
DiscourseURL.redirectTo(backup.get('link'));
let link = backup.get('filename');
ajax("/admin/backups/" + link, { type: "PUT" })
.then(() => {
bootbox.alert(I18n.t("admin.backups.operations.download.alert"));
});
}
},
@ -48,7 +51,7 @@ export default Ember.Controller.extend({
ajax("/admin/backups/readonly", {
type: "PUT",
data: { enable: enable }
}).then(function() {
}).then(() => {
site.set("isReadOnly", enable);
});
}

View File

@ -2,6 +2,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import IncomingEmail from 'admin/models/incoming-email';
import computed from 'ember-addons/ember-computed-decorators';
import { longDate } from 'discourse/lib/formatter';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
@ -12,6 +13,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
load(id) {
return IncomingEmail.find(id).then(result => this.set("model", result));
},
loadFromBounced(id) {
return IncomingEmail.findByBounced(id)
.then(result => this.set("model", result))
.catch(error => {
this.send("closeModal");
popupAjaxError(error);
});
}
});

View File

@ -19,6 +19,11 @@ IncomingEmail.reopenClass({
return ajax(`/admin/email/incoming/${id}.json`);
},
findByBounced(id) {
return ajax(`/admin/email/incoming_from_bounced/${id}.json`);
},
findAll(filter, offset) {
filter = filter || {};
offset = offset || 0;

View File

@ -1,2 +1,14 @@
import showModal from 'discourse/lib/show-modal';
import AdminEmailLogs from 'admin/routes/admin-email-logs';
export default AdminEmailLogs.extend({ status: "bounced" });
export default AdminEmailLogs.extend({
status: "bounced",
actions: {
showIncomingEmail(id) {
showModal('admin-incoming-email', { admin: true });
this.controllerFor("modals/admin-incoming-email").loadFromBounced(id);
}
}
});

View File

@ -1,5 +1,5 @@
export default Discourse.Route.extend({
redirect: function() {
redirect() {
this.replaceWith('adminFlags.list', 'active');
}
});

View File

@ -6,11 +6,6 @@ export default Discourse.Route.extend({
this.render('admin/templates/logs/staff-action-logs', {into: 'adminLogs'});
},
setupController: function(controller) {
controller.resetFilters();
controller.refresh();
},
actions: {
showDetailsModal(model) {
showModal('admin-staff-action-log-details', { model, admin: true });

View File

@ -28,6 +28,14 @@ export default Discourse.Route.extend({
showSuspendModal(model) {
showModal('admin-suspend-user', { model, admin: true });
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
},
viewActionLogs(username) {
const controller = this.controllerFor('adminLogs.staffActionLogs');
this.transitionTo('adminLogs.staffActionLogs').then(() => {
controller.set('filters', Ember.Object.create());
controller._changeFilters({ target_user: username });
});
}
}
});

View File

@ -261,8 +261,8 @@
<thead>
<tr>
<th class="title">{{top_referrers.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
<th>{{number top_referrers.ytitles.num_clicks}}</th>
<th>{{number top_referrers.ytitles.num_topics}}</th>
<th>{{top_referrers.ytitles.num_clicks}}</th>
<th>{{top_referrers.ytitles.num_topics}}</th>
</tr>
</thead>
{{#each top_referrers.data as |r|}}

View File

@ -28,7 +28,7 @@
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td><a {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.logs.none'}}</td></tr>

View File

@ -7,6 +7,9 @@
{{i18n 'admin.user.show_public_profile'}}
{{/link-to}}
{{/if}}
{{#if model.can_view_action_logs}}
{{d-button action="viewActionLogs" actionParam=model.username icon="list-alt" label="admin.user.action_logs"}}
{{/if}}
{{#if model.active}}
{{#if model.can_impersonate}}
{{d-button class="btn-danger" action="impersonate" icon="crosshairs" label="admin.impersonate.title" title="admin.impersonate.help"}}

View File

@ -9,6 +9,7 @@
// Stuff we need to load first
//= require ./discourse/lib/utilities
//= require ./discourse/lib/page-visible
//= require ./discourse/lib/ajax
//= require ./discourse/lib/text
//= require ./discourse/lib/hash

View File

@ -68,7 +68,8 @@ export default Ember.Component.extend({
if (loadOnebox && loadOnebox.then) {
loadOnebox.then( () => {
this._updatePost(lookupCache(this.get('composer.title')));
const v = lookupCache(this.get('composer.title'));
this._updatePost(v ? v : link);
}).finally(() => {
this.set('composer.loading', false);
Ember.run.schedule('afterRender', () => { this.$('input').putCursorAtEnd(); });

View File

@ -8,6 +8,6 @@ export default Ember.Component.extend({
}.property('tagRecord.id'),
href: function() {
return '/tags/' + this.get('tagRecord.id');
return Discourse.getURL('/tags/' + this.get('tagRecord.id'));
}.property('tagRecord.id'),
});

View File

@ -21,5 +21,13 @@ export default buildCategoryPanel('settings', {
{name: I18n.t('category.sort_ascending'), value: 'true'},
{name: I18n.t('category.sort_descending'), value: 'false'}
];
},
@computed
availableViews() {
return [
{name: I18n.t('filters.latest.title'), value: 'latest'},
{name: I18n.t('filters.top.title'), value: 'top'}
];
}
});

View File

@ -1,11 +1,17 @@
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import DiscourseURL from 'discourse/lib/url';
import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors';
export default Ember.Component.extend({
tagName: 'ul',
classNameBindings: [':nav', ':nav-pills'],
id: 'navigation-bar',
init() {
this._super();
this.set('connectors', renderedConnectorsFor("extra-nav-item", null, this));
},
@computed("filterMode", "navItems")
selectedNavItem(filterMode, navItems){
var item = navItems.find(i => i.get('filterMode').indexOf(filterMode) === 0);

View File

@ -29,7 +29,7 @@
The list of disabled plugins is returned via the `Site` singleton.
**/
import { connectorsFor } from 'discourse/lib/plugin-connectors';
import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors';
export default Ember.Component.extend({
tagName: 'span',
@ -38,14 +38,9 @@ export default Ember.Component.extend({
init() {
this._super();
const name = this.get('name');
if (name) {
const args = this.get('args');
const connectors = connectorsFor(name).filter(con => {
return con.connectorClass.shouldRender(args, this);
});
this.set('connectors', connectors);
this.set('connectors', renderedConnectorsFor(name, args, this));
}
}
});

View File

@ -3,24 +3,26 @@ import { escapeExpression } from 'discourse/lib/utilities';
const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g;
const REGEXP_USERNAME_PREFIX = /(user:|@)/ig;
const REGEXP_CATEGORY_PREFIX = /(category:|#)/ig;
const REGEXP_GROUP_PREFIX = /group:/ig;
const REGEXP_BADGE_PREFIX = /badge:/ig;
const REGEXP_TAGS_PREFIX = /tags?:/ig;
const REGEXP_IN_PREFIX = /in:/ig;
const REGEXP_STATUS_PREFIX = /status:/ig;
const REGEXP_MIN_POST_COUNT_PREFIX = /min_post_count:/ig;
const REGEXP_POST_TIME_PREFIX = /(before|after):/ig;
const REGEXP_USERNAME_PREFIX = /^(user:|@)/ig;
const REGEXP_CATEGORY_PREFIX = /^(category:|#)/ig;
const REGEXP_GROUP_PREFIX = /^group:/ig;
const REGEXP_BADGE_PREFIX = /^badge:/ig;
const REGEXP_TAGS_PREFIX = /^(tags?:|#(?=[a-z0-9\-]+::tag))/ig;
const REGEXP_IN_PREFIX = /^in:/ig;
const REGEXP_STATUS_PREFIX = /^status:/ig;
const REGEXP_MIN_POST_COUNT_PREFIX = /^min_post_count:/ig;
const REGEXP_POST_TIME_PREFIX = /^(before|after):/ig;
const REGEXP_TAGS_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::tag\s?$)/ig;
const REGEXP_IN_MATCH = /in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig;
const REGEXP_SPECIAL_IN_LIKES_MATCH = /in:likes/ig;
const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /in:private/ig;
const REGEXP_SPECIAL_IN_WIKI_MATCH = /in:wiki/ig;
const REGEXP_CATEGORY_SLUG = /(\#[a-zA-Z0-9\-:]+)/ig;
const REGEXP_CATEGORY_ID = /(category:[0-9]+)/ig;
const REGEXP_POST_TIME_WHEN = /(before|after)/ig;
const REGEXP_IN_MATCH = /^in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig;
const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/ig;
const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/ig;
const REGEXP_SPECIAL_IN_WIKI_MATCH = /^in:wiki/ig;
const REGEXP_CATEGORY_SLUG = /^(\#[a-zA-Z0-9\-:]+)/ig;
const REGEXP_CATEGORY_ID = /^(category:[0-9]+)/ig;
const REGEXP_POST_TIME_WHEN = /^(before|after)/ig;
export default Em.Component.extend({
classNames: ['search-advanced-options'],
@ -48,8 +50,8 @@ export default Em.Component.extend({
init() {
this._super();
this._init();
Ember.run.scheduleOnce('afterRender', () => {
this._init();
this._update();
});
},
@ -228,7 +230,7 @@ export default Em.Component.extend({
if (match.length !== 0) {
const existingInput = _.isArray(tags) ? tags.join(',') : tags;
const userInput = match[0].replace(REGEXP_TAGS_PREFIX, '');
const userInput = match[0].replace(REGEXP_TAGS_REPLACE, '');
if (existingInput !== userInput) {
this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(',') : []);

View File

@ -1,6 +1,7 @@
import computed from 'ember-addons/ember-computed-decorators';
import { on } from 'ember-addons/ember-computed-decorators';
import TextField from 'discourse/components/text-field';
import { applySearchAutocomplete } from "discourse/lib/search";
export default TextField.extend({
@computed('searchService.searchContextEnabled')
@ -10,10 +11,13 @@ export default TextField.extend({
@on("didInsertElement")
becomeFocused() {
const $searchInput = this.$();
applySearchAutocomplete($searchInput, this.siteSettings);
if (!this.get('hasAutofocus')) { return; }
// iOS is crazy, without this we will not be
// at the top of the page
$(window).scrollTop(0);
this.$().focus();
$searchInput.focus();
}
});

View File

@ -1,11 +1,15 @@
import { iconHTML } from 'discourse-common/helpers/fa-icon';
import Combobox from 'discourse-common/components/combo-box';
import { on, observes } from 'ember-addons/ember-computed-decorators';
import { observes } from 'ember-addons/ember-computed-decorators';
export default Combobox.extend({
none: "topic.controls",
@on('init')
init() {
this._super();
this._createContent();
},
_createContent() {
const content = [];
const topic = this.get('topic');

View File

@ -404,13 +404,15 @@ export default Ember.Controller.extend({
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
save(force) {
const composer = this.get('model');
if (this.get("disableSubmit")) return;
// Clear the warning state if we're not showing the checkbox anymore
if (!this.get('showWarning')) {
this.set('model.isWarning', false);
}
const composer = this.get('model');
if (composer.get('cantSubmitPost')) {
this.set('lastValidatedAt', Date.now());
return;
@ -561,14 +563,17 @@ export default Ember.Controller.extend({
return;
}
this.setProperties({ showEditReason: false, editReason: null, scopedCategoryId: null });
// If we show the subcategory list, scope the categories drop down to
// the category we opened the composer with.
if (this.siteSettings.show_subcategory_list && opts.draftKey !== 'reply_as_new_topic') {
this.set('scopedCategoryId', opts.categoryId);
if (opts.categoryId && opts.draftKey !== 'reply_as_new_topic') {
const category = this.site.categories.findBy('id', opts.categoryId);
if (category && (category.get('show_subcategory_list') || category.get('parentCategory.show_subcategory_list'))) {
this.set('scopedCategoryId', opts.categoryId);
}
}
this.setProperties({ showEditReason: false, editReason: null });
// If we want a different draft than the current composer, close it and clear our model.
if (composerModel &&
opts.draftKey !== composerModel.draftKey &&

View File

@ -1,17 +1,15 @@
import { ajax } from 'discourse/lib/ajax';
import debounce from 'discourse/lib/debounce';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { setting } from 'discourse/lib/computed';
import { on } from 'ember-addons/ember-computed-decorators';
import { emailValid } from 'discourse/lib/utilities';
import InputValidation from 'discourse/models/input-validation';
import PasswordValidation from "discourse/mixins/password-validation";
import UsernameValidation from "discourse/mixins/username-validation";
export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, {
export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, UsernameValidation, {
login: Ember.inject.controller(),
uniqueUsernameValidation: null,
globalNicknameExists: false,
complete: false,
accountPasswordConfirm: 0,
accountChallenge: 0,
@ -24,8 +22,6 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, {
hasAuthOptions: Em.computed.notEmpty('authOptions'),
canCreateLocal: setting('enable_local_logins'),
showCreateForm: Em.computed.or('hasAuthOptions', 'canCreateLocal'),
maxUsernameLength: setting('max_username_length'),
minUsernameLength: setting('min_username_length'),
resetForm() {
// We wrap the fields in a structure so we can assign a value
@ -35,7 +31,6 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, {
accountUsername: '',
accountPassword: '',
authOptions: null,
globalNicknameExists: false,
complete: false,
formSubmitted: false,
rejectedEmails: [],
@ -167,128 +162,6 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, {
}
}.observes('emailValidation', 'accountEmail'),
fetchExistingUsername: debounce(function() {
const self = this;
Discourse.User.checkUsername(null, this.get('accountEmail')).then(function(result) {
if (result.suggestion && (Ember.isEmpty(self.get('accountUsername')) || self.get('accountUsername') === self.get('authOptions.username'))) {
self.set('accountUsername', result.suggestion);
self.set('prefilledUsername', result.suggestion);
}
});
}, 500),
usernameMatch: function() {
if (this.usernameNeedsToBeValidatedWithEmail()) {
if (this.get('emailValidation.failed')) {
if (this.shouldCheckUsernameMatch()) {
return this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: I18n.t('user.username.enter_email')
}));
} else {
return this.set('uniqueUsernameValidation', InputValidation.create({ failed: true }));
}
} else if (this.shouldCheckUsernameMatch()) {
this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: I18n.t('user.username.checking')
}));
return this.checkUsernameAvailability();
}
}
}.observes('accountEmail'),
basicUsernameValidation: function() {
this.set('uniqueUsernameValidation', null);
if (this.get('accountUsername') === this.get('prefilledUsername')) {
return InputValidation.create({
ok: true,
reason: I18n.t('user.username.prefilled')
});
}
// If blank, fail without a reason
if (Ember.isEmpty(this.get('accountUsername'))) {
return InputValidation.create({
failed: true
});
}
// If too short
if (this.get('accountUsername').length < Discourse.SiteSettings.min_username_length) {
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.too_short')
});
}
// If too long
if (this.get('accountUsername').length > this.get('maxUsernameLength')) {
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.too_long')
});
}
this.checkUsernameAvailability();
// Let's check it out asynchronously
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.checking')
});
}.property('accountUsername'),
shouldCheckUsernameMatch: function() {
return !Ember.isEmpty(this.get('accountUsername')) && this.get('accountUsername').length >= this.get('minUsernameLength');
},
checkUsernameAvailability: debounce(function() {
const _this = this;
if (this.shouldCheckUsernameMatch()) {
return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(function(result) {
_this.set('isDeveloper', false);
if (result.available) {
if (result.is_developer) {
_this.set('isDeveloper', true);
}
return _this.set('uniqueUsernameValidation', InputValidation.create({
ok: true,
reason: I18n.t('user.username.available')
}));
} else {
if (result.suggestion) {
return _this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: I18n.t('user.username.not_available', result)
}));
} else if (result.errors) {
return _this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: result.errors.join(' ')
}));
} else {
return _this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: I18n.t('user.username.enter_email')
}));
}
}
});
}
}, 500),
// Actually wait for the async name check before we're 100% sure we're good to go
usernameValidation: function() {
const basicValidation = this.get('basicUsernameValidation');
const uniqueUsername = this.get('uniqueUsernameValidation');
return uniqueUsername ? uniqueUsername : basicValidation;
}.property('uniqueUsernameValidation', 'basicUsernameValidation'),
usernameNeedsToBeValidatedWithEmail() {
return( this.get('globalNicknameExists') || false );
},
@on('init')
fetchConfirmationValue() {
return ajax('/users/hp.json').then(json => {

View File

@ -7,7 +7,9 @@ export const queryParams = {
search: { replace: true, refreshModel: true },
max_posts: { replace: true, refreshModel: true },
q: { replace: true, refreshModel: true },
tags: { replace: true }
tags: { replace: true },
before: { replace: true, refreshModel: true},
bumped_before: { replace: true, refreshModel: true}
};
// Basic controller options
@ -19,4 +21,14 @@ const controllerOpts = {
// Aliases for the values
controllerOpts.queryParams.forEach(p => controllerOpts[p] = Ember.computed.alias(`discoveryTopics.${p}`));
export default Ember.Controller.extend(controllerOpts);
const Controller = Ember.Controller.extend(controllerOpts);
export const addDiscoveryQueryParam = function(p, opts) {
queryParams[p] = opts;
const cOpts = {};
cOpts[p] = Ember.computed.alias(`discoveryTopics.${p}`);
cOpts["queryParams"] = Object.keys(queryParams);
Controller.reopen(cOpts);
};
export default Controller;

View File

@ -19,6 +19,10 @@ const controllerOpts = {
expandGloballyPinned: false,
expandAllPinned: false,
resetParams() {
this.setProperties({ order: "default", ascending: false });
},
actions: {
changeSort(sortBy) {
@ -43,8 +47,7 @@ const controllerOpts = {
refresh() {
const filter = this.get('model.filter');
this.setProperties({ order: "default", ascending: false });
this.resetParams();
// Don't refresh if we're still loading
if (this.get('discovery.loading')) { return; }

View File

@ -0,0 +1,66 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
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 UsernameValidation from "discourse/mixins/username-validation";
import { findAll as findLoginMethods } from 'discourse/models/login-method';
export default Ember.Controller.extend(PasswordValidation, UsernameValidation, {
invitedBy: Ember.computed.alias('model.invited_by'),
email: Ember.computed.alias('model.email'),
accountUsername: Ember.computed.alias('model.username'),
passwordRequired: Ember.computed.notEmpty('accountPassword'),
successMessage: null,
errorMessage: null,
inviteImageUrl: getUrl('/images/envelope.svg'),
@computed
welcomeTitle() {
return I18n.t('invites.welcome_to', {site_name: this.siteSettings.title});
},
@computed('email')
yourEmailMessage(email) {
return I18n.t('invites.your_email', {email: email});
},
@computed
externalAuthsEnabled() {
return findLoginMethods(this.siteSettings, this.capabilities, this.site.isMobileDevice).length > 0;
},
@computed('usernameValidation.failed', 'passwordValidation.failed')
submitDisabled(usernameFailed, passwordFailed) {
return usernameFailed || passwordFailed;
},
actions: {
submit() {
ajax({
url: `/invites/show/${this.get('model.token')}.json`,
type: 'PUT',
data: {
username: this.get('accountUsername'),
password: this.get('accountPassword')
}
}).then(result => {
if (result.success) {
this.set('successMessage', result.message || I18n.t('invites.success'));
this.set('redirectTo', result.redirect_to);
DiscourseURL.redirectTo(result.redirect_to || '/');
} else {
if (result.errors && result.errors.password && result.errors.password.length > 0) {
this.get('rejectedPasswords').pushObject(this.get('accountPassword'));
this.get('rejectedPasswordsMessages').set(this.get('accountPassword'), result.errors.password[0]);
}
if (result.message) {
this.set('errorMessage', result.message);
}
}
}).catch(response => {
throw response;
});
}
}
});

View File

@ -6,9 +6,13 @@ import { findAll } from 'discourse/models/login-method';
import { escape } from 'pretty-text/sanitizer';
// This is happening outside of the app via popup
const AuthErrors =
['requires_invite', 'awaiting_approval', 'awaiting_confirmation', 'admin_not_allowed_from_ip_address',
'not_allowed_from_ip_address'];
const AuthErrors = [
'requires_invite',
'awaiting_approval',
'awaiting_activation',
'admin_not_allowed_from_ip_address',
'not_allowed_from_ip_address'
];
export default Ember.Controller.extend(ModalFunctionality, {

View File

@ -1,11 +1,9 @@
import computed from "ember-addons/ember-computed-decorators";
import NavigationDefaultController from 'discourse/controllers/navigation/default';
import { setting } from 'discourse/lib/computed';
export default NavigationDefaultController.extend({
subcategoryListSetting: setting('show_subcategory_list'),
showingParentCategory: Em.computed.none('category.parentCategory'),
showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'),
showingSubcategoryList: Em.computed.and('category.show_subcategory_list', 'showingParentCategory'),
@computed("showingSubcategoryList", "category", "noSubcategories")
navItems(showingSubcategoryList, category, noSubcategories) {

View File

@ -15,6 +15,7 @@ addBulkButton('showNotificationLevel', 'notification_level');
addBulkButton('resetRead', 'reset_read');
addBulkButton('unlistTopics', 'unlist_topics');
addBulkButton('showTagTopics', 'change_tags');
addBulkButton('showAppendTagTopics', 'append_tags');
// Modal for performing bulk actions on topics
export default Ember.Controller.extend(ModalFunctionality, {
@ -78,6 +79,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
actions: {
showTagTopics() {
this.set('tags', '');
this.set('action', 'changeTags');
this.set('label', 'change_tags');
this.set('title', 'choose_new_tags');
this.send('changeBulkTemplate', 'bulk-tag');
},
@ -85,6 +89,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.performAndRefresh({type: 'change_tags', tags: this.get('tags')});
},
showAppendTagTopics() {
this.set('tags', '');
this.set('action', 'appendTags');
this.set('label', 'append_tags');
this.set('title', 'choose_append_tags');
this.send('changeBulkTemplate', 'bulk-tag');
},
appendTags() {
this.performAndRefresh({type: 'append_tags', tags: this.get('tags')});
},
showChangeCategory() {
this.send('changeBulkTemplate', 'modal/bulk-change-category');
this.set('modal.modalClass', 'topic-bulk-actions-modal full');

View File

@ -313,6 +313,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
const quoteState = this.get('quoteState');
const postStream = this.get('model.postStream');
if (!postStream) return;
const quotedPost = postStream.findLoadedPost(quoteState.postId);
const quotedText = Quote.build(quotedPost, quoteState.buffer);
@ -912,6 +913,7 @@ 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:show-topic', topic);
});
} else {
if (topic.get('isPrivateMessage') &&

View File

@ -0,0 +1,6 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import renderTags from 'discourse/lib/render-tags';
export default registerUnbound('discourse-tags', function(topic, params) {
return new Handlebars.SafeString(renderTags(topic, params));
});

View File

@ -4,10 +4,31 @@ export default {
name: 'localization',
after: 'inject-objects',
initialize: function(container) {
enableVerboseLocalization() {
let counter = 0;
let keys = {};
let t = I18n.t;
I18n.noFallbacks = true;
I18n.t = I18n.translate = function(scope, value){
let current = keys[scope];
if (!current) {
current = keys[scope] = ++counter;
let message = "Translation #" + current + ": " + scope;
if (!_.isEmpty(value)) {
message += ", parameters: " + JSON.stringify(value);
}
Em.Logger.info(message);
}
return t.apply(I18n, [scope, value]) + " (#" + current + ")";
};
},
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
if (siteSettings.verbose_localization) {
I18n.enable_verbose_localization();
this.enableVerboseLocalization();
}
// Merge any overrides into our object
@ -16,24 +37,26 @@ export default {
const v = overrides[k];
// Special case: Message format keys are functions
if (/\_MF$/.test(k)) {
if (/_MF$/.test(k)) {
k = k.replace(/^[a-z_]*js\./, '');
I18n._compiledMFs[k] = new Function('transKey', `return (${v})(transKey);`);
return;
}
k = k.replace('admin_js', 'js');
const segs = k.split('.');
let node = I18n.translations[I18n.locale];
let i = 0;
for (; node && i<segs.length-1; i++) {
for (; i < segs.length - 1; i++) {
if (!(segs[i] in node)) node[segs[i]] = {};
node = node[segs[i]];
}
if (node && i === segs.length-1) {
node[segs[segs.length-1]] = v;
}
node[segs[segs.length-1]] = v;
});
}
};

View File

@ -1,4 +1,6 @@
// Initialize the message bus to receive messages.
import pageVisible from 'discourse/lib/page-visible';
export default {
name: "message-bus",
after: 'inject-objects',
@ -36,9 +38,21 @@ export default {
messageBus.ajax = function(opts) {
opts.headers = opts.headers || {};
opts.headers['X-Shared-Session-Key'] = $('meta[name=shared_session_key]').attr('content');
if (pageVisible()) {
opts.headers['Discourse-Visible'] = "true";
}
return $.ajax(opts);
};
} else {
messageBus.ajax = function(opts) {
opts.headers = opts.headers || {};
if (pageVisible()) {
opts.headers['Discourse-Visible'] = "true";
}
return $.ajax(opts);
};
messageBus.baseUrl = Discourse.getURL('/');
}

View File

@ -55,7 +55,7 @@ export default {
const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id});
if (staleIndex === -1) {
// this gets a bit tricky, uread pms are bumped to front
// this gets a bit tricky, unread pms are bumped to front
let insertPosition = 0;
if (lastNotification.notification_type !== 6) {
insertPosition = _.findIndex(oldNotifications, n => n.notification_type !== 6 || n.read);

View File

@ -15,7 +15,7 @@ export default {
if (currentUser) {
const username = currentUser.get('username');
DiscourseURL.rewrite(new RegExp(`^/users/${username}/?$`, "i"), `/users/${username}/summary`);
DiscourseURL.rewrite(new RegExp(`^/users/${username}/?$`, "i"), `/users/${username}/activity`);
}
DiscourseURL.rewrite(/^\/users\/([^\/]+)\/?$/, "/users/$1/activity");

View File

@ -1,3 +1,5 @@
import pageVisible from 'discourse/lib/page-visible';
let _trackView = false;
let _transientHeader = null;
@ -14,6 +16,7 @@ export function viewTrackingRequired() {
for performance reasons. Also automatically adjusts the URL to support installs
in subfolders.
**/
export function ajax() {
let url, args;
let ajaxObj;
@ -47,6 +50,10 @@ export function ajax() {
args.headers['Discourse-Track-View'] = "true";
}
if (pageVisible()) {
args.headers['Discourse-Visible'] = "true";
}
args.success = (data, textStatus, xhr) => {
if (xhr.getResponseHeader('Discourse-Readonly')) {
Ember.run(() => Discourse.Site.currentProp('isReadOnly', true));

View File

@ -82,7 +82,7 @@ export default function(options) {
let prevTerm = null;
// input is handled differently
const isInput = this[0].tagName === "INPUT";
const isInput = this[0].tagName === "INPUT" && !options.treatAsTextarea;
let inputSelectedItems = [];
function closeAutocomplete() {
@ -175,8 +175,10 @@ export default function(options) {
wrap.width(width);
}
if(options.single) {
this.css("width","100%");
if(options.single && !options.width) {
this.css("width", "100%");
} else if (options.width) {
this.css("width", options.width);
} else {
this.width(150);
}
@ -238,6 +240,7 @@ export default function(options) {
var pos = null;
var vOffset = 0;
var hOffset = 0;
if (isInput) {
pos = {
left: 0,
@ -250,7 +253,9 @@ export default function(options) {
pos: completeStart,
key: options.key
});
hOffset = 27;
if (options.treatAsTextarea) vOffset = -32;
}
div.css({
left: "-1000px"
@ -258,7 +263,7 @@ export default function(options) {
me.parent().append(div);
if (!isInput) {
if (!isInput && !options.treatAsTextarea) {
vOffset = div.height();
if ((window.innerHeight - me.outerHeight() - $("header.d-header").innerHeight()) < vOffset) {

View File

@ -0,0 +1,12 @@
// for android we test webkit
var hiddenProperty = document.hidden !== undefined ? "hidden" : (
document.webkitHidden !== undefined ? "webkitHidden" : undefined
);
export default function() {
if (hiddenProperty !== undefined){
return !document[hiddenProperty];
} else {
return document && document.hasFocus;
}
};

View File

@ -13,9 +13,14 @@ import { addFlagProperty } from 'discourse/components/site-header';
import { addPopupMenuOptionsCallback } from 'discourse/controllers/composer';
import { extraConnectorClass } from 'discourse/lib/plugin-connectors';
import { addPostSmallActionIcon } from 'discourse/widgets/post-small-action';
import { addDiscoveryQueryParam } from 'discourse/controllers/discovery-sortable';
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';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = 0.8;
const PLUGIN_API_VERSION = '0.8.5';
class PluginApi {
constructor(version, container) {
@ -377,12 +382,108 @@ class PluginApi {
addPostSmallActionIcon(key, icon) {
addPostSmallActionIcon(key, icon);
}
/**
* Register an additional query param with topic discovery,
* this allows for filters on the topic list
*
**/
addDiscoveryQueryParam(param, options) {
addDiscoveryQueryParam(param, options);
}
/**
* Register a callback to be called every time tags render
* highest priority callbacks are called first
* example:
*
* callback = function(topic, params) {
* if (topic.get("created_at") < "2000-00-01") {
* return "<span class='discourse-tag'>ANCIENT</span>"
* }
* }
*
* api.addTagsHtmlCallback(callback, {priority: 100});
*
**/
addTagsHtmlCallback(callback, options) {
addTagsHtmlCallback(callback, options);
};
/**
* Adds a glyph to user menu after bookmarks
* WARNING: there is limited space there
*
* example:
*
* api.addUserMenuGlyph({
* label: 'awesome.label',
* className: 'my-class',
* icon: 'my-icon',
* href: `/some/path`
* });
*
*/
addUserMenuGlyph(glyph) {
addUserMenuGlyph(glyph);
};
/**
* Adds a callback to be called before rendering any post that
* that returns custom classes to add to the post
*
* Example:
*
* addPostClassesCallback((atts) => {if (atts.post_number == 1) return ["first"];})
**/
addPostClassesCallback(callback) {
addPostClassesCallback(callback);
}
/**
*
* Adds a callback to be executed on the "transformed" post that is passed to the post
* widget.
*
* This allows you to apply transformations on the actual post that is about to be rendered.
*
* Example:
*
* addPostTransformCallback((t)=>{
* // post number 7 is overrated, don't show it ever
* if (t.post_number === 7) { t.cooked = ""; }
* })
*/
addPostTransformCallback(callback) {
addPostTransformCallback(callback);
}
}
let _pluginv01;
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
function cmpVersions (a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, '').split('.');
var segmentsB = b.replace(regExStrip0, '').split('.');
var l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}
}
return segmentsA.length - segmentsB.length;
}
function getPluginApi(version) {
version = parseFloat(version);
if (version <= PLUGIN_API_VERSION) {
version = version.toString();
if (cmpVersions(version,PLUGIN_API_VERSION) <= 0) {
if (!_pluginv01) {
_pluginv01 = new PluginApi(version, Discourse.__container__);
}

View File

@ -92,6 +92,13 @@ export function connectorsFor(outletName) {
return _connectorCache[outletName] || [];
}
export function renderedConnectorsFor(outletName, args, context) {
return connectorsFor(outletName).filter(con => {
return con.connectorClass.shouldRender(args, context);
});
}
export function rawConnectorsFor(outletName) {
if (!_rawConnectorCache) { buildRawConnectorCache(); }
return _rawConnectorCache[outletName] || [];

View File

@ -1,5 +1,3 @@
import { h } from 'virtual-dom';
export default function renderTag(tag, params) {
params = params || {};
tag = Handlebars.Utils.escapeExpression(tag);
@ -19,19 +17,3 @@ export default function renderTag(tag, params) {
return val;
};
export function tagNode(tag, params) {
const classes = ['tag-' + tag, 'discourse-tag'];
const tagName = params.tagName || "a";
if (Discourse.SiteSettings.tag_style || params.style) {
classes.push(params.style || Discourse.SiteSettings.tag_style);
}
if (tagName === 'a') {
const href = Discourse.getURL(`/tags/${tag}`);
return h(tagName, { className: classes.join(' '), attributes: { href } }, tag);
} else {
return h(tagName, { className: classes.join(' ') }, tag);
}
}

View File

@ -0,0 +1,57 @@
import renderTag from 'discourse/lib/render-tag';
let callbacks = null;
let priorities = null;
export function addTagsHtmlCallback(callback, options) {
callbacks = callbacks || [];
priorities = priorities || [];
const priority = (options && options.priority) || 0;
let i = 0;
while(i < priorities.length && priorities[i] > priority) {
i += 1;
}
priorities.splice(i, 0, priority);
callbacks.splice(i, 0, callback);
};
export default function(topic, params){
let tags = topic.tags;
let buffer = "";
if (params && params.mode === "list") {
tags = topic.get("visibleListTags");
}
let customHtml = null;
if (callbacks) {
callbacks.forEach((c) => {
const html = c(topic, params);
if (html) {
if (customHtml) {
customHtml += html;
} else {
customHtml = html;
}
}
});
}
if (customHtml || (tags && tags.length > 0)) {
buffer = "<div class='discourse-tags'>";
if (tags) {
for(let i=0; i<tags.length; i++){
buffer += renderTag(tags[i]);
}
}
if (customHtml) {
buffer += customHtml;
}
buffer += "</div>";
}
return buffer;
};

View File

@ -1,4 +1,4 @@
function applicable() {
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) &&
@ -7,19 +7,15 @@ function applicable() {
}
// we can't tell what the actual visible window height is
// because we cannot account for the height of the mobile keyboard
// and any other mobile autocomplete UI that may appear
// so let's be conservative here rather than trying to max out every
// available pixel of height for the editor
function calcHeight(composingTopic) {
const winHeight = window.innerHeight;
// Hard code some known iOS resolutions
switch(winHeight) {
case 460: return composingTopic ? 250 : 260;
case 559: return composingTopic ? 325 : 308;
case 627:
case 628: return 360;
}
const ratio = composingTopic ? 0.54 : 0.6;
const min = composingTopic ? 300 : 350;
const ratio = composingTopic ? 0.45 : 0.45;
const min = composingTopic ? 300 : 300;
return Math.max(parseInt(winHeight*ratio), min);
}
@ -32,7 +28,7 @@ export function isWorkaroundActive() {
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
function positioningWorkaround($fixedElement) {
if (!applicable()) {
if (!isAppleDevice()) {
return;
}

View File

@ -1,9 +1,14 @@
import { ajax } from 'discourse/lib/ajax';
import { findRawTemplate } from 'discourse/lib/raw-templates';
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
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';
export function translateResults(results, opts) {
const User = require('discourse/models/user').default;
const Category = require('discourse/models/category').default;
const Post = require('discourse/models/post').default;
const Topic = require('discourse/models/topic').default;
@ -124,3 +129,39 @@ export function isValidSearchTerm(searchTerm) {
return false;
}
};
export function applySearchAutocomplete($input, siteSettings, appEvents) {
const afterComplete = function() {
if (appEvents) {
appEvents.trigger("search-autocomplete:after-complete");
}
};
$input.autocomplete({
template: findRawTemplate('category-tag-autocomplete'),
key: '#',
width: '100%',
treatAsTextarea: true,
transformComplete(obj) {
if (obj.model) {
return Category.slugFor(obj.model, SEPARATOR);
} else {
return `${obj.text}${TAG_HASHTAG_POSTFIX}`;
}
},
dataSource(term) {
return searchCategoryTag(term, siteSettings);
},
afterComplete
});
$input.autocomplete({
template: findRawTemplate('user-selector-autocomplete'),
key: "@",
width: '100%',
treatAsTextarea: true,
transformComplete: v => v.username || v.name,
dataSource: term => userSearch({ term, includeGroups: true }),
afterComplete
});
};

View File

@ -326,6 +326,11 @@ const DiscourseURL = Ember.Object.extend({
if (opts.replaceURL) {
this.replaceState(path);
} else {
const discoveryTopics = this.controllerFor('discovery/topics');
if (discoveryTopics) {
discoveryTopics.resetParams();
}
router.router.updateURL(path);
}

View File

@ -0,0 +1,106 @@
import InputValidation from 'discourse/models/input-validation';
import debounce from 'discourse/lib/debounce';
import { setting } from 'discourse/lib/computed';
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Mixin.create({
uniqueUsernameValidation: null,
maxUsernameLength: setting('max_username_length'),
minUsernameLength: setting('min_username_length'),
fetchExistingUsername: debounce(function() {
const self = this;
Discourse.User.checkUsername(null, this.get('accountEmail')).then(function(result) {
if (result.suggestion && (Ember.isEmpty(self.get('accountUsername')) || self.get('accountUsername') === self.get('authOptions.username'))) {
self.set('accountUsername', result.suggestion);
self.set('prefilledUsername', result.suggestion);
}
});
}, 500),
@computed('accountUsername')
basicUsernameValidation(accountUsername) {
this.set('uniqueUsernameValidation', null);
if (accountUsername === this.get('prefilledUsername')) {
return InputValidation.create({
ok: true,
reason: I18n.t('user.username.prefilled')
});
}
// If blank, fail without a reason
if (Ember.isEmpty(accountUsername)) {
return InputValidation.create({
failed: true
});
}
// If too short
if (accountUsername.length < Discourse.SiteSettings.min_username_length) {
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.too_short')
});
}
// If too long
if (accountUsername.length > this.get('maxUsernameLength')) {
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.too_long')
});
}
this.checkUsernameAvailability();
// Let's check it out asynchronously
return InputValidation.create({
failed: true,
reason: I18n.t('user.username.checking')
});
},
shouldCheckUsernameAvailability: function() {
return !Ember.isEmpty(this.get('accountUsername')) && this.get('accountUsername').length >= this.get('minUsernameLength');
},
checkUsernameAvailability: debounce(function() {
if (this.shouldCheckUsernameAvailability()) {
return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(result => {
this.set('isDeveloper', false);
if (result.available) {
if (result.is_developer) {
this.set('isDeveloper', true);
}
return this.set('uniqueUsernameValidation', InputValidation.create({
ok: true,
reason: I18n.t('user.username.available')
}));
} else {
if (result.suggestion) {
return this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: I18n.t('user.username.not_available', result)
}));
} else {
return this.set('uniqueUsernameValidation', InputValidation.create({
failed: true,
reason: result.errors ? result.errors.join(' ') : I18n.t('user.username.not_available_no_suggestion')
}));
}
}
});
}
}, 500),
// Actually wait for the async name check before we're 100% sure we're good to go
@computed('uniqueUsernameValidation', 'basicUsernameValidation')
usernameValidation() {
const basicValidation = this.get('basicUsernameValidation');
const uniqueUsername = this.get('uniqueUsernameValidation');
return uniqueUsername ? uniqueUsername : basicValidation;
}
});

View File

@ -70,7 +70,7 @@ const Category = RestModel.extend({
@computed("topic_count")
moreTopics(topicCount) {
return topicCount > Discourse.SiteSettings.category_featured_topics;
return topicCount > (this.get('num_featured_topics') || 2);
},
save() {
@ -102,7 +102,10 @@ const Category = RestModel.extend({
allowed_tag_groups: this.get('allowed_tag_groups'),
sort_order: this.get('sort_order'),
sort_ascending: this.get('sort_ascending'),
topic_featured_link_allowed: this.get('topic_featured_link_allowed')
topic_featured_link_allowed: this.get('topic_featured_link_allowed'),
show_subcategory_list: this.get('show_subcategory_list'),
num_featured_topics: this.get('num_featured_topics'),
default_view: this.get('default_view')
},
type: id ? 'PUT' : 'POST'
});
@ -148,7 +151,7 @@ const Category = RestModel.extend({
@computed("topics")
featuredTopics(topics) {
if (topics && topics.length) {
return topics.slice(0, Discourse.SiteSettings.category_featured_topics || 2);
return topics.slice(0, this.get('num_featured_topics') || 2);
}
},

View File

@ -98,13 +98,22 @@ TopicList.reopenClass({
if (!result) { return; }
// Stitch together our side loaded data
const categories = Discourse.Category.list(),
users = Model.extractByKey(result.users, Discourse.User);
users = Model.extractByKey(result.users, Discourse.User),
groups = Model.extractByKey(result.primary_groups, Ember.Object);
return result.topic_list.topics.map(function (t) {
t.category = categories.findBy('id', t.category_id);
t.posters.forEach(function(p) {
p.user = users[p.user_id];
p.extraClasses = p.extras;
if (p.primary_group_id) {
p.primary_group = groups[p.primary_group_id];
if (p.primary_group) {
p.extraClasses = `${p.extraClasses||''} group-${p.primary_group.name}`;
}
}
});
if (t.participants) {
t.participants.forEach(function(p) {

View File

@ -13,9 +13,9 @@ export default {
app.DiscoveryCategoryNoneController = DiscoverySortableController.extend();
app.DiscoveryCategoryWithIDController = DiscoverySortableController.extend();
app.DiscoveryCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryParentCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryCategoryNoneRoute = buildCategoryRoute('latest', {no_subcategories: true});
app.DiscoveryCategoryRoute = buildCategoryRoute('default');
app.DiscoveryParentCategoryRoute = buildCategoryRoute('default');
app.DiscoveryCategoryNoneRoute = buildCategoryRoute('default', {no_subcategories: true});
const site = Discourse.Site.current();
site.get('filters').forEach(filter => {

View File

@ -142,4 +142,8 @@ export default function() {
this.route('tagGroups', {path: '/tag_groups', resetNamespace: true}, function() {
this.route('show', {path: '/:id'});
});
this.route('invites', { path: '/invites', resetNamespace: true }, function() {
this.route('show', { path: '/:token' });
});
}

View File

@ -6,7 +6,7 @@ import CategoryList from 'discourse/models/category-list';
import Category from 'discourse/models/category';
// A helper function to create a category route with parameters
export default (filter, params) => {
export default (filterArg, params) => {
return Discourse.Route.extend({
queryParams,
@ -37,9 +37,13 @@ export default (filter, params) => {
this._retrieveTopicList(model.category, transition)]);
},
filter(category) {
return filterArg === 'default' ? (category.get('default_view') || 'latest') : filterArg;
},
_setupNavigation(category) {
const noSubcategories = params && !!params.no_subcategories,
filterMode = `c/${Discourse.Category.slugFor(category)}${noSubcategories ? "/none" : ""}/l/${filter}`;
filterMode = `c/${Discourse.Category.slugFor(category)}${noSubcategories ? "/none" : ""}/l/${this.filter(category)}`;
this.controllerFor('navigation/category').setProperties({
category,
@ -51,7 +55,7 @@ export default (filter, params) => {
_createSubcategoryList(category) {
this._categoryList = null;
if (Em.isNone(category.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
if (Em.isNone(category.get('parentCategory')) && category.get('show_subcategory_list')) {
return CategoryList.listForParent(this.store, category).then(list => this._categoryList = list);
}
@ -60,7 +64,7 @@ export default (filter, params) => {
},
_retrieveTopicList(category, transition) {
const listFilter = `c/${Discourse.Category.slugFor(category)}/l/${filter}`,
const listFilter = `c/${Discourse.Category.slugFor(category)}/l/${this.filter(category)}`,
findOpts = filterQueryParams(transition.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
@ -72,8 +76,8 @@ export default (filter, params) => {
},
titleToken() {
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title'),
category = this.currentModel.category;
const category = this.currentModel.category,
filterText = I18n.t('filters.' + this.filter(category).replace('/', '.') + '.title');
return I18n.t('filters.with_category', { filter: filterText, category: category.get('name') });
},
@ -82,7 +86,8 @@ export default (filter, params) => {
const topics = this.get('topics'),
category = model.category,
canCreateTopic = topics.get('can_create_topic'),
canCreateTopicOnCategory = category.get('permission') === PermissionType.FULL;
canCreateTopicOnCategory = category.get('permission') === PermissionType.FULL,
filter = this.filter(category);
this.controllerFor('navigation/category').setProperties({
canCreateTopicOnCategory: canCreateTopicOnCategory,

View File

@ -0,0 +1,13 @@
import PreloadStore from 'preload-store';
export default Discourse.Route.extend({
titleToken() {
return I18n.t('invites.accept_title');
},
model(params) {
if (PreloadStore.get("invite_info")) {
return PreloadStore.getAndRemove("invite_info").then(json => _.merge(params, json));
}
}
});

View File

@ -4,7 +4,7 @@ export default Discourse.Route.extend({
const { currentUser } = this;
const viewingMe = (currentUser && currentUser.get('username') === this.modelFor('user').get('username'));
const destination = viewingMe ? 'user.summary' : 'userActivity';
const destination = viewingMe ? 'userActivity' : 'user.summary';
// HACK: Something with the way the user card intercepts clicks seems to break how the
// transition into a user's activity works. This makes the back button work on mobile

View File

@ -1,7 +1,7 @@
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Object.extend({
searchContextEnabled: false,
searchContextEnabled: false, // checkbox to scope search
searchContext: null,
term: null,
highlightTerm: null,

View File

@ -1,5 +1,5 @@
<p>{{i18n "topics.bulk.choose_new_tags"}}</p>
<p>{{i18n (concat "topics.bulk." title)}}</p>
<p>{{tag-chooser tags=tags categoryId=categoryId}}</p>
{{d-button action="changeTags" disabled=emptyTags label="topics.bulk.change_tags"}}
{{d-button action=action disabled=emptyTags label=(concat "topics.bulk." label)}}

View File

@ -19,6 +19,15 @@
</label>
</section>
{{#unless category.parent_category_id}}
<section class="field">
<label>
{{input type="checkbox" checked=category.show_subcategory_list}}
{{i18n "category.show_subcategory_list"}}
</label>
</section>
{{/unless}}
<section class="field">
<label>
{{input type="checkbox" checked=category.all_topics_wiki}}
@ -47,6 +56,13 @@
</label>
</section>
<section class="field default-view-field">
<label>
{{i18n "category.default_view"}}
{{combo-box valueAttribute="value" content=availableViews value=category.default_view}}
</label>
</section>
{{#if emailInEnabled}}
<section class='field'>
<label>
@ -66,8 +82,19 @@
{{plugin-outlet name="category-email-in" args=(hash category=category)}}
{{/if}}
<section class="field num-featured-topics-fields">
<label>
{{#if category.parent_category_id}}
{{i18n "category.subcategory_num_featured_topics"}}
{{else}}
{{i18n "category.num_featured_topics"}}
{{/if}}
{{text-field value=category.num_featured_topics}}
</label>
</section>
{{#if showPositionInput}}
<section class='field'>
<section class='field position-fields'>
<label>
{{i18n 'category.position'}}
{{text-field value=category.position class="position-input"}}

View File

@ -19,13 +19,7 @@
</tr>
<tr>
{{category-link topic.category}}
{{#if topic.tags}}
<div class="discourse-tags">
{{#each topic.visibleListTags as |tag|}}
{{discourse-tag tag}}
{{/each}}
</div>
{{/if}}
{{discourse-tags topic mode="list"}}
</tr>
</td>
<td class="topic-stats">

View File

@ -1,5 +1,8 @@
{{#each navItems as |navItem|}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
{{custom-html name="extraNavItem"}}
{{plugin-outlet name="extra-nav-item" connectorTagName="li"}}
{{custom-html name="extraNavItem" tagName="li"}}
{{!- this is done to avoid DIV in the UL, originally {{plugin-outlet name="extra-nav-item"}}
{{#each connectors as |c|}}
{{plugin-connector connector=c class=c.classNames tagName="li" args=(hash category=category filterMode=filterMode)}}
{{/each}}

View File

@ -13,7 +13,7 @@
<div class="control-group pull-left">
<label class="control-label" for="search-in-category">{{i18n "search.advanced.in_category.label"}}</label>
<div class="controls">
{{category-selector categories=searchedTerms.category single="true"}}
{{category-selector categories=searchedTerms.category single="true" canReceiveUpdates="true"}}
</div>
</div>
</div>

View File

@ -0,0 +1 @@
{{outlet}}

View File

@ -0,0 +1,55 @@
<div class="container invites-show clearfix">
<h2>{{welcomeTitle}}</h2>
<div class="two-col">
<div class="col-image">
<img src={{inviteImageUrl}}>
</div>
<div class="col-form">
<p>{{i18n 'invites.invited_by'}}</p>
<p>{{user-info user=invitedBy}}</p>
{{#if successMessage}}
<p>{{successMessage}}</p>
{{else}}
<p>{{{yourEmailMessage}}}
{{#if externalAuthsEnabled}}
{{i18n 'invites.social_login_available'}}
{{/if}}
</p>
<form>
<label>{{i18n 'user.username.title'}}</label>
<div class="input username-input">
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}}
&nbsp;{{input-tip validation=usernameValidation id="username-validation"}}
</div>
<label>{{i18n 'invites.password_label'}}</label>
<div class="input password-input">
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
&nbsp;{{input-tip validation=passwordValidation}}
</div>
<div class="instructions">
<div class="caps-lock-warning {{unless capsLockOn 'invisible'}}"><i class="fa fa-exclamation-triangle"></i> {{i18n 'login.caps_lock_warning'}}</div>
</div>
<button class='btn btn-primary' {{action "submit"}} disabled={{submitDisabled}}>{{i18n 'invites.accept_invite'}}</button>
{{#if errorMessage}}
<br/><br/>
<div class='alert alert-error'>{{errorMessage}}</div>
{{/if}}
</form>
{{/if}}
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<td class='posters'>
{{#each posters as |poster|}}
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extraClasses}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
{{/each}}
</td>

View File

@ -18,14 +18,7 @@
{{/if}}
</span>
{{#if topic.tags}}
<div class='discourse-tags'>
{{#each topic.visibleListTags as |tag|}}
{{discourse-tag tag}}
{{/each}}
</div>
{{/if}}
{{raw-plugin-outlet name="topic-list-tags"}}
{{discourse-tags topic mode="list"}}
{{#if expandPinned}}
{{raw "list/topic-excerpt" topic=topic}}
{{/if}}

View File

@ -32,15 +32,7 @@
</div>
{{/unless}}
{{#if topic.tags}}
<div class='discourse-tags'>
{{#each topic.visibleListTags as |tag|}}
{{discourse-tag tag}}
{{/each}}
</div>
{{/if}}
{{raw-plugin-outlet name="topic-list-tags"}}
{{discourse-tags topic mode="list"}}
<div class="pull-right">
<div class='num activity last'>

View File

@ -1,24 +1,26 @@
<div class="container">
<div class='directory'>
{{#load-more selector=".directory .user" action="loadMore"}}
<div class="container">
<div class='directory'>
<div class='clearfix user-controls'>
{{period-chooser period=period}}
{{text-field value=nameInput placeholderKey="directory.filter_name" class="filter-name no-blur"}}
</div>
{{#conditional-loading-spinner condition=model.loading}}
{{#if model.length}}
<div class='total-rows'>{{i18n "directory.total_rows" count=model.totalRows}}</div>
{{#each model as |item|}}
{{directory-item tagName="div" class="user" item=item showTimeRead=showTimeRead}}
{{/each}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{else}}
<div class='clearfix'></div>
<p>{{i18n "directory.no_results"}}</p>
{{/if}}
{{/conditional-loading-spinner}}
<div class='clearfix user-controls'>
{{period-chooser period=period}}
{{text-field value=nameInput placeholderKey="directory.filter_name" class="filter-name no-blur"}}
</div>
{{#conditional-loading-spinner condition=model.loading}}
{{#if model.length}}
<div class='total-rows'>{{i18n "directory.total_rows" count=model.totalRows}}</div>
{{#each model as |item|}}
{{directory-item tagName="div" class="user" item=item showTimeRead=showTimeRead}}
{{/each}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{else}}
<div class='clearfix'></div>
<p>{{i18n "directory.no_results"}}</p>
{{/if}}
{{/conditional-loading-spinner}}
</div>
</div>
{{/load-more}}

View File

@ -15,7 +15,7 @@
noSubcategories=noSubcategories
hideSubcategories=showingSubcategoryList}}
{{navigation-bar navItems=navItems filterMode=filterMode}}
{{navigation-bar navItems=navItems filterMode=filterMode category=category}}
{{#if currentUser}}
{{category-notifications-button category=category}}

View File

@ -21,6 +21,7 @@
{{i18n 'user.messages.archive'}}
{{/link-to}}
</li>
{{plugin-outlet name="user-messages-nav" connectorTagName='li' args=(hash model=model)}}
{{#each model.groups as |group|}}
{{#if group.has_messages}}
<li>

View File

@ -26,7 +26,11 @@ createWidget('small-user-list', {
users = users.concat(avatarAtts(currentUser));
}
let description = I18n.t(atts.description, { icons: '' });
let description = null;
if (atts.description) {
description = I18n.t(atts.description, { icons: '' });
}
// oddly post_url is on the user
let postUrl;
@ -38,7 +42,13 @@ createWidget('small-user-list', {
if (postUrl) {
description = h('a', { attributes: { href: Discourse.getURL(postUrl) } }, description);
}
return [icons, description, '.'];
let buffer = [icons];
if (description) {
buffer.push(description);
buffer.push(".");
}
return buffer;
}
}
});

View File

@ -36,7 +36,7 @@ export default createWidget('hamburger-menu', {
const { currentUser } = this;
const links = [{ route: 'admin', className: 'admin-link', icon: 'wrench', label: 'admin_title' },
{ route: 'adminFlags',
{ href: '/admin/flags/active',
className: 'flagged-posts-link',
icon: 'flag',
label: 'flags_title',
@ -117,11 +117,10 @@ export default createWidget('hamburger-menu', {
listCategories() {
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
const showSubcatList = this.siteSettings.show_subcategory_list;
const isStaff = Discourse.User.currentProp('staff');
const categories = Discourse.Category.list().reject((c) => {
if (showSubcatList && c.get('parent_category_id')) { return true; }
if (c.get('parentCategory.show_subcategory_list')) { return true; }
if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; }
return false;
});

View File

@ -1,15 +1,15 @@
import { createWidget } from 'discourse/widgets/widget';
import { applyDecorators, createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom';
import { iconNode } from 'discourse/helpers/fa-icon-node';
import DiscourseURL from 'discourse/lib/url';
import RawHtml from 'discourse/widgets/raw-html';
import { tagNode } from 'discourse/lib/render-tag';
import renderTags from 'discourse/lib/render-tags';
import { topicFeaturedLinkNode } from 'discourse/lib/render-topic-featured-link';
export default createWidget('header-topic-info', {
tagName: 'div.extra-info-wrapper',
html(attrs) {
html(attrs, state) {
const topic = attrs.topic;
const heading = [];
@ -45,13 +45,14 @@ export default createWidget('header-topic-info', {
title.push(this.attach('category-link', { category }));
}
const extra = [];
if (this.siteSettings.tagging_enabled) {
const tags = topic.get('tags') || [];
if (tags.length) {
extra.push(h('div.list-tags', tags.map(tagNode)));
}
let extra = [];
const tags = renderTags(topic);
if (tags && tags.length > 0) {
extra.push(new RawHtml({html: tags}));
}
extra = extra.concat(applyDecorators(this, 'after-tags', attrs, state));
if (this.siteSettings.topic_featured_link_enabled) {
const featured = topicFeaturedLinkNode(attrs.topic);
if (featured) {

View File

@ -3,6 +3,7 @@ import { iconNode } from 'discourse/helpers/fa-icon-node';
import { avatarImg } from 'discourse/widgets/post';
import DiscourseURL from 'discourse/lib/url';
import { wantsNewWindow } from 'discourse/lib/intercept-click';
import { applySearchAutocomplete } from "discourse/lib/search";
import { h } from 'virtual-dom';
@ -163,6 +164,8 @@ createWidget('header-buttons', {
}
});
const forceContextEnabled = ['category', 'user', 'private_messages'];
export default createWidget('header', {
tagName: 'header.d-header.clearfix',
buildKey: () => `header`,
@ -172,7 +175,6 @@ export default createWidget('header', {
searchVisible: false,
hamburgerVisible: false,
userVisible: false,
contextEnabled: false,
ringBackdrop: true
};
@ -192,6 +194,19 @@ export default createWidget('header', {
flagCount: attrs.flagCount })];
if (state.searchVisible) {
const contextType = this.searchContextType();
if (state.searchContextType !== contextType) {
state.contextEnabled = undefined;
state.searchContextType = contextType;
}
if (state.contextEnabled === undefined) {
if (forceContextEnabled.includes(contextType)) {
state.contextEnabled = true;
}
}
panels.push(this.attach('search-menu', { contextEnabled: state.contextEnabled }));
} else if (state.hamburgerVisible) {
panels.push(this.attach('hamburger-menu'));
@ -244,7 +259,11 @@ export default createWidget('header', {
this.updateHighlight();
if (this.state.searchVisible) {
Ember.run.schedule('afterRender', () => $('#search-term').focus().select());
Ember.run.schedule('afterRender', () => {
const $searchInput = $('#search-term');
$searchInput.focus().select();
applySearchAutocomplete($searchInput, this.siteSettings, this.appEvents);
});
}
},
@ -289,6 +308,7 @@ export default createWidget('header', {
},
searchMenuContextChanged(value) {
this.state.contextType = this.register.lookup('search-service:main').get('contextType');
this.state.contextEnabled = value;
},
@ -318,6 +338,16 @@ export default createWidget('header', {
}
break;
}
},
searchContextType() {
const service = this.register.lookup('search-service:main');
if (service) {
const ctx = service.get('searchContext');
if (ctx) {
return Ember.get(ctx, 'type');
}
}
}
});

View File

@ -3,6 +3,21 @@ import transformPost from 'discourse/lib/transform-post';
import { Placeholder } from 'discourse/lib/posts-with-placeholders';
import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
let transformCallbacks = null;
function postTransformCallbacks(transformed) {
if (transformCallbacks === null) {
return;
}
for(let i=0; i < transformCallbacks.length; i++) {
transformCallbacks[i].call(this, transformed);
}
}
export function addPostTransformCallback(callback){
transformCallbacks = transformCallbacks || [];
transformCallbacks.push(callback);
};
const CLOAKING_ENABLED = !window.inTestEnv;
const DAY = 1000 * 60 * 60 * 24;
@ -96,6 +111,8 @@ export default createWidget('post-stream', {
transformed.height = _heights[post.id];
transformed.cloaked = _cloaked[post.id];
postTransformCallbacks(transformed);
if (transformed.isSmallAction) {
result.push(this.attach('post-small-action', transformed, { model: post }));
} else {

View File

@ -376,6 +376,12 @@ createWidget('post-article', {
});
let addPostClassesCallbacks = null;
export function addPostClassesCallback(callback) {
addPostClassesCallbacks = addPostClassesCallbacks || [];
addPostClassesCallbacks.push(callback);
}
export default createWidget('post', {
buildKey: attrs => `post-${attrs.id}`,
shadowTree: true,
@ -405,6 +411,14 @@ export default createWidget('post', {
} else {
classNames.push('regular');
}
if (addPostClassesCallbacks) {
for(let i=0; i<addPostClassesCallbacks.length; i++) {
let pluginClasses = addPostClassesCallbacks[i].call(this, attrs);
if (pluginClasses) {
classNames.push.apply(classNames, pluginClasses);
}
}
}
return classNames;
},

View File

@ -5,6 +5,15 @@ import { createWidget } from 'discourse/widgets/widget';
createWidget('search-term', {
tagName: 'input',
buildId: () => 'search-term',
buildKey: (attrs) => `search-term-${attrs.id}`,
defaultState() {
this.appEvents.on("search-autocomplete:after-complete", () => {
this.state.afterAutocomplete = true;
});
return { afterAutocomplete: false };
},
buildAttributes(attrs) {
return { type: 'text',
@ -14,7 +23,11 @@ createWidget('search-term', {
keyUp(e) {
if (e.which === 13) {
return this.sendWidgetAction('fullSearch');
if (this.state.afterAutocomplete) {
this.state.afterAutocomplete = false;
} else {
return this.sendWidgetAction('fullSearch');
}
}
const val = this.attrs.value;

View File

@ -46,7 +46,7 @@ function postResult(result, link, term) {
}
createSearchResult('user', 'path', function(u) {
return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', u.username ];
return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ];
});
createSearchResult('topic', 'url', function(result, term) {

View File

@ -148,7 +148,12 @@ export default createWidget('search-menu', {
},
html(attrs) {
searchData.contextEnabled = attrs.contextEnabled;
if (searchData.contextEnabled !== attrs.contextEnabled) {
searchData.contextEnabled = attrs.contextEnabled;
this.triggerSearch();
} else {
searchData.contextEnabled = attrs.contextEnabled;
}
return this.attach('menu-panel', { maxWidth: 500, contents: () => this.panelContents() });
},
@ -170,6 +175,7 @@ export default createWidget('search-menu', {
},
searchContextChanged(enabled) {
// This indicates the checkbox has been clicked, NOT that the context has changed.
searchData.typeFilter = null;
this.sendWidgetAction('searchMenuContextChanged', enabled);
searchData.contextEnabled = enabled;

View File

@ -1,6 +1,13 @@
import { createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom';
let extraGlyphs;
export function addUserMenuGlyph(glyph) {
extraGlyphs = extraGlyphs || [];
extraGlyphs.push(glyph);
}
createWidget('user-menu-links', {
tagName: 'div.menu-links-header',
@ -13,10 +20,18 @@ createWidget('user-menu-links', {
isAnon;
const path = attrs.path;
const glyphs = [{ label: 'user.bookmarks',
const glyphs = [];
if (extraGlyphs) {
// yes glyphs.push(...extraGlyphs) is nicer, but pulling in
// _toConsumableArray seems totally uneeded here
glyphs.push.apply(glyphs, extraGlyphs);
}
glyphs.push({ label: 'user.bookmarks',
className: 'user-bookmarks-link',
icon: 'bookmark',
href: `${path}/activity/bookmarks` }];
href: `${path}/activity/bookmarks` });
if (siteSettings.enable_private_messages) {
glyphs.push({ label: 'user.private_messages',

View File

@ -1,12 +1,3 @@
//= depend_on 'client.ar.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:ar) %>
I18n.pluralizationRules['ar'] = function (n) {
if (n == 0) return "zero";
if (n == 1) return "one";
if (n == 2) return "two";
if (n%100 >= 3 && n%100 <= 10) return "few";
if (n%100 >= 11 && n%100 <= 99) return "many";
return "other";
};

View File

@ -1,10 +1,3 @@
//= depend_on 'client.cs.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:cs) %>
I18n.pluralizationRules['cs'] = function (n) {
if (n == 0) return ["zero", "none", "other"];
if (n == 1) return "one";
if (n >= 2 && n <= 4) return "few";
return "other";
};

View File

@ -0,0 +1,3 @@
//= depend_on 'client.el.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:el) %>

View File

@ -1,7 +1,3 @@
//= depend_on 'client.fa_IR.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:fa_IR) %>
I18n.pluralizationRules['fa_IR'] = function (n) {
return "other";
};

View File

@ -1,48 +1,17 @@
/*global I18n:true */
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if ( this === undefined || this === null ) {
throw new TypeError( '"this" is null or not defined' );
}
var length = this.length >>> 0; // Hack to convert object.length to a UInt32
fromIndex = +fromIndex || 0;
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
// Instantiate the object
var I18n = I18n || {};
// Set default locale to english
I18n.defaultLocale = "en";
// Set default handling of translation fallbacks to false
I18n.fallbacks = false;
// Set default separator
I18n.defaultSeparator = ".";
// Set default pluralization rule
I18n.pluralizationRules = {
en: function(n) {
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
}
};
// Set current locale to null
I18n.locale = null;
@ -50,44 +19,10 @@ I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
I18n.fallbackRules = {};
I18n.SEPARATOR = ".";
I18n.noFallbacks = false;
I18n.pluralizationRules = {
en: function(n) {
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
},
"zh_CN": function(n) {
return n === 0 ? ["zero", "none", "other"] : "other";
},
"zh_TW": function(n) {
return n === 0 ? ["zero", "none", "other"] : "other";
},
"ko": function(n) {
return n === 0 ? ["zero", "none", "other"] : "other";
}
};
I18n.getFallbacks = function(locale) {
if (locale === I18n.defaultLocale) {
return [];
} else if (!I18n.fallbackRules[locale]) {
var rules = [],
components = locale.split("-");
for (var l = 1; l < components.length; l++) {
rules.push(components.slice(0, l).join("-"));
}
rules.push(I18n.defaultLocale);
I18n.fallbackRules[locale] = rules;
}
return I18n.fallbackRules[locale];
};
I18n.isValidNode = function(obj, node, undefined) {
return obj[node] !== null && obj[node] !== undefined;
};
@ -95,25 +30,24 @@ I18n.isValidNode = function(obj, node, undefined) {
function checkExtras(origScope, sep, extras) {
if (!extras || extras.length === 0) { return; }
for (var i=0; i<extras.length; i++) {
for (var i = 0; i < extras.length; i++) {
var messages = extras[i];
scope = origScope.split(sep);
if (scope[0] === 'js') {
scope.shift();
}
if (scope[0] === 'js') { scope.shift(); }
while (messages && scope.length > 0) {
currentScope = scope.shift();
messages = messages[currentScope];
}
if (messages !== undefined) {
return messages;
}
if (messages !== undefined) { return messages; }
}
}
I18n.lookup = function(scope, options) {
options = options || {};
var lookupInitialScope = scope,
translations = this.prepareOptions(I18n.translations),
locale = options.locale || I18n.currentLocale(),
@ -123,16 +57,16 @@ I18n.lookup = function(scope, options) {
options = this.prepareOptions(options);
if (typeof scope === "object") {
scope = scope.join(this.defaultSeparator);
scope = scope.join(this.SEPARATOR);
}
if (options.scope) {
scope = options.scope.toString() + this.defaultSeparator + scope;
scope = options.scope.toString() + this.SEPARATOR + scope;
}
var origScope = "" + scope;
scope = origScope.split(this.defaultSeparator);
scope = origScope.split(this.SEPARATOR);
while (messages && scope.length > 0) {
currentScope = scope.shift();
@ -140,24 +74,11 @@ I18n.lookup = function(scope, options) {
}
if (messages === undefined) {
messages = checkExtras(origScope, this.defaultSeparator, this.extras);
messages = checkExtras(origScope, this.SEPARATOR, this.extras);
}
if (messages === undefined) {
if (I18n.fallbacks) {
var fallbacks = this.getFallbacks(locale);
for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
if (messages !== undefined) {
break;
}
}
}
if (messages === undefined && this.isValidNode(options, "defaultValue")) {
messages = options.defaultValue;
}
messages = options.defaultValue;
}
return messages;
@ -193,14 +114,13 @@ I18n.prepareOptions = function() {
I18n.interpolate = function(message, options) {
options = this.prepareOptions(options);
var matches = message.match(this.PLACEHOLDER),
placeholder,
value,
name;
if (!matches) {
return message;
}
if (!matches) { return message; }
for (var i = 0; placeholder = matches[i]; i++) {
name = placeholder.replace(this.PLACEHOLDER, "$1");
@ -219,24 +139,25 @@ I18n.interpolate = function(message, options) {
};
I18n.translate = function(scope, options) {
options = this.prepareOptions(options);
var translation = this.lookup(scope, options);
// Fallback to the default locale
if (!translation && this.currentLocale() !== this.defaultLocale && !this.noFallbacks) {
options.locale = this.defaultLocale;
translation = this.lookup(scope, options);
}
if (!translation && this.currentLocale() !== 'en' && !this.noFallbacks) {
options.locale = 'en';
translation = this.lookup(scope, options);
if (!this.noFallbacks) {
if (!translation && this.currentLocale() !== this.defaultLocale) {
options.locale = this.defaultLocale;
translation = this.lookup(scope, options);
}
if (!translation && this.currentLocale() !== 'en') {
options.locale = 'en';
translation = this.lookup(scope, options);
}
}
try {
if (typeof translation === "object") {
if (typeof options.count === "number") {
return this.pluralize(options.count, scope, options);
return this.pluralize(translation, scope, options);
} else {
return translation;
}
@ -248,158 +169,16 @@ I18n.translate = function(scope, options) {
}
};
I18n.localize = function(scope, value) {
switch (scope) {
case "currency":
return this.toCurrency(value);
case "number":
scope = this.lookup("number.format");
return this.toNumber(value, scope);
case "percentage":
return this.toPercentage(value);
default:
if (scope.match(/^(date|time)/)) {
return this.toTime(scope, value);
} else {
return value.toString();
}
}
};
I18n.parseDate = function(date) {
var matches, convertedDate;
// we have a date, so just return it.
if (typeof date === "object") {
return date;
}
// it matches the following formats:
// yyyy-mm-dd
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ssZ
// yyyy-mm-dd[ T]hh:mm::ss+0000
//
matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
if (matches) {
for (var i = 1; i <= 6; i++) {
matches[i] = parseInt(matches[i], 10) || 0;
}
// month starts on 0
matches[2] -= 1;
if (matches[7]) {
convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
} else {
convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
}
} else if (typeof date === "number") {
// UNIX timestamp
convertedDate = new Date();
convertedDate.setTime(date);
} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
// a valid javascript format with timezone info
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
} else {
// an arbitrary javascript string
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
}
return convertedDate;
};
I18n.toTime = function(scope, d) {
var date = this.parseDate(d),
format = this.lookup(scope);
if (date.toString().match(/invalid/i)) {
return date.toString();
}
if (!format) {
return date.toString();
}
return this.strftime(date, format);
};
I18n.strftime = function(date, format) {
var options = this.lookup("date");
if (!options) {
return date.toString();
}
options.meridian = options.meridian || ["AM", "PM"];
var weekDay = date.getDay(),
day = date.getDate(),
year = date.getFullYear(),
month = date.getMonth() + 1,
hour = date.getHours(),
hour12 = hour,
meridian = hour > 11 ? 1 : 0,
secs = date.getSeconds(),
mins = date.getMinutes(),
offset = date.getTimezoneOffset(),
absOffsetHours = Math.floor(Math.abs(offset / 60)),
absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60),
timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes);
if (hour12 > 12) {
hour12 = hour12 - 12;
} else if (hour12 === 0) {
hour12 = 12;
}
var padding = function(n) {
var s = "0" + n.toString();
return s.substr(s.length - 2);
};
var f = format;
f = f.replace("%a", options.abbr_day_names[weekDay]);
f = f.replace("%A", options.day_names[weekDay]);
f = f.replace("%b", options.abbr_month_names[month]);
f = f.replace("%B", options.month_names[month]);
f = f.replace("%d", padding(day));
f = f.replace("%e", day);
f = f.replace("%-d", day);
f = f.replace("%H", padding(hour));
f = f.replace("%-H", hour);
f = f.replace("%I", padding(hour12));
f = f.replace("%-I", hour12);
f = f.replace("%m", padding(month));
f = f.replace("%-m", month);
f = f.replace("%M", padding(mins));
f = f.replace("%-M", mins);
f = f.replace("%p", options.meridian[meridian]);
f = f.replace("%S", padding(secs));
f = f.replace("%-S", secs);
f = f.replace("%w", weekDay);
f = f.replace("%y", padding(year));
f = f.replace("%-y", padding(year).replace(/^0+/, ""));
f = f.replace("%Y", year);
f = f.replace("%z", timezoneoffset);
return f;
};
I18n.toNumber = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
{precision: 3, separator: this.SEPARATOR, delimiter: ",", strip_insignificant_zeros: false}
);
var negative = number < 0,
string = Math.abs(number).toFixed(options.precision).toString(),
parts = string.split("."),
parts = string.split(this.SEPARATOR),
precision,
buffer = [],
formattedNumber;
@ -437,23 +216,6 @@ I18n.toNumber = function(number, options) {
return formattedNumber;
};
I18n.toCurrency = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.currency.format"),
this.lookup("number.format"),
{unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
);
number = this.toNumber(number, options);
number = options.format
.replace("%u", options.unit)
.replace("%n", number)
;
return number;
};
I18n.toHumanSize = function(number, options) {
var kb = 1024,
size = number,
@ -488,18 +250,6 @@ I18n.toHumanSize = function(number, options) {
return number;
};
I18n.toPercentage = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.percentage.format"),
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ""}
);
number = this.toNumber(number, options);
return number + "%";
};
I18n.pluralizer = function(locale) {
var pluralizer = this.pluralizationRules[locale];
if (pluralizer !== undefined) return pluralizer;
@ -514,16 +264,11 @@ I18n.findAndTranslateValidNode = function(keys, translation) {
return null;
};
I18n.pluralize = function(count, scope, options) {
var translation;
try { translation = this.lookup(scope, options); } catch (error) {}
if (!translation) { return this.missingTranslation(scope); }
I18n.pluralize = function(translation, scope, options) {
options = this.prepareOptions(options);
options.count = count.toString();
var count = options.count.toString();
var pluralizer = this.pluralizer(this.currentLocale());
var pluralizer = this.pluralizer(options.locale || this.currentLocale());
var key = pluralizer(Math.abs(count));
var keys = ((typeof key === "object") && (key instanceof Array)) ? key : [key];
@ -534,52 +279,14 @@ I18n.pluralize = function(count, scope, options) {
};
I18n.missingTranslation = function(scope, key) {
var message = '[' + this.currentLocale() + "." + scope;
if (key) { message += "." + key; }
var message = '[' + this.currentLocale() + this.SEPARATOR + scope;
if (key) { message += this.SEPARATOR + key; }
return message + ']';
};
I18n.currentLocale = function() {
return (I18n.locale || I18n.defaultLocale);
return I18n.locale || I18n.defaultLocale;
};
// shortcuts
I18n.t = I18n.translate;
I18n.l = I18n.localize;
I18n.p = I18n.pluralize;
I18n.enable_verbose_localization = function(){
var counter = 0;
var keys = {};
var t = I18n.t;
I18n.noFallbacks = true;
I18n.t = I18n.translate = function(scope, value){
var current = keys[scope];
if(!current) {
current = keys[scope] = ++counter;
var message = "Translation #" + current + ": " + scope;
if (!_.isEmpty(value)) {
message += ", parameters: " + JSON.stringify(value);
}
Em.Logger.info(message);
}
return t.apply(I18n, [scope, value]) + " (t" + current + ")";
};
};
I18n.verbose_localization_session = function(){
sessionStorage.setItem("verbose_localization", "true");
I18n.enable_verbose_localization();
return true;
}
try {
if(sessionStorage && sessionStorage.getItem("verbose_localization")) {
I18n.enable_verbose_localization();
}
} catch(e){
// we don't care really, can happen if cookies disabled
}

View File

@ -1,7 +1,3 @@
//= depend_on 'client.ja.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:ja) %>
I18n.pluralizationRules['ja'] = function (n) {
return n === 0 ? ["zero", "none", "other"] : "other";
};

View File

@ -1,9 +1,3 @@
//= depend_on 'client.ro.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:ro) %>
I18n.pluralizationRules['ro'] = function (n) {
if (n == 1) return "one";
if (n === 0 || n % 100 >= 1 && n % 100 <= 19) return "few";
return "other";
};

View File

@ -1,9 +1,3 @@
//= depend_on 'client.ru.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:ru) %>
I18n.pluralizationRules['ru'] = function (n) {
if (n % 10 == 1 && n % 100 != 11) return "one";
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) return "few";
return "other";
};

View File

@ -1,9 +1,3 @@
//= depend_on 'client.sk.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:sk) %>
I18n.pluralizationRules['sk'] = function (n) {
if (n == 1) return "one";
if (n >= 2 && n <= 4) return "few";
return "other";
};

View File

@ -1,5 +1,3 @@
//= depend_on 'client.tr_TR.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:tr_TR) %>
I18n.pluralizationRules['tr_TR'] = function(n) { return "other"; }

View File

@ -1,10 +1,3 @@
//= depend_on 'client.uk.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:uk) %>
I18n.pluralizationRules['uk'] = function (n) {
if (n == 0) return ["zero", "none", "other"];
if (n % 10 == 1 && n % 100 != 11) return "one";
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return "few";
return "other"; // TODO: should be "many" but is not defined in translations
};

View File

@ -148,34 +148,6 @@ div.ac-wrap {
}
}
.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 {
input[type=text] {
width: 50px;
}
label {
font-size: .929em;
}
}
}
#reply-control {
.composer-loading {
position: absolute;

View File

@ -72,6 +72,28 @@ $input-width: 220px;
}
}
.invites-show {
.two-col {
position: relative;
}
.col-image {
position: absolute;
top: 0;
left: 0;
}
form {
margin-top: 24px;
button.btn-primary {
margin-top: 10px;
}
label {
font-weight: bold;
}
}
}
// alternate login / create new account buttons should be de-emphasized

View File

@ -184,6 +184,10 @@
display: block;
padding: 5px;
transition: all linear .15s;
.user-results {
color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%));
}
}
&:hover a:not(.badge-notification) {

View File

@ -300,3 +300,33 @@
margin-bottom: 10px;
}
}
.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 {
input[type=text] {
width: 50px;
}
}
.auto-close-fields label {
font-size: .929em;
}
}

View File

@ -139,6 +139,12 @@ aside.onebox {
margin-right: 10px;
}
// tighten bottom margin on last para
p:last-child {
margin-bottom: 4px;
}
// twitter fixes
.tweet-images {
display: block;
clear: both;
@ -150,6 +156,7 @@ aside.onebox {
float: none;
}
}
}
}

View File

@ -15,7 +15,7 @@
-o-transform: rotate(#{$degrees}deg);
transform: rotate(#{$degrees}deg);
filter: progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=#{cos($degrees)}, M12=-#{sin($degrees)}, M21=#{sin($degrees)}, M22=#{cos($degrees)});
filter: "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=#{cos($degrees)}, M12=-#{sin($degrees)}, M21=#{sin($degrees)}, M22=#{cos($degrees)})";
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=#{cos($degrees)}, M12=-#{sin($degrees)}, M21=#{sin($degrees)}, M22=#{cos($degrees)})";
zoom: 1;
}

View File

@ -48,6 +48,8 @@
}
.public-user-fields {
margin-top: 8px;
margin-bottom: 8px;
.user-field-name {
font-weight: bold;
}

View File

@ -8,7 +8,7 @@
%nav {
margin-left: 0;
list-style: none;
li > a {
li a {
display: block;
text-decoration: none;
}
@ -56,7 +56,7 @@
&:last-of-type {
border-bottom: 0;
}
> a {
a {
margin: 0;
padding: 13px 13px 13px 30px;
font-size: 1.143em;

View File

@ -59,16 +59,46 @@
}
.password-reset {
.password-reset, .invites-show {
.col-form {
padding-top: 40px;
padding-left: 20px;
}
h2 {
margin-bottom: 12px;
}
.password-reset-img {
.col-image img {
width: 200px;
height: 200px;
}
}
.password-reset {
.col-form {
padding-top: 40px;
}
}
.invites-show {
padding-top: 20px;
.two-col {
margin-top: 30px;
}
.col-image {
width: 200px;
img {
width: 200px;
}
}
.col-form {
margin-left: 200px;
.inline-invite-img {
display: none;
}
}
form {
label, .input {
margin-left: 20px;
}
}
}

View File

@ -83,7 +83,7 @@
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%);
padding-top: 10px;
height: 20px;
width: 757px;
max-width: 757px;
}
#topic-progress-wrapper {

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