Version bump

This commit is contained in:
Neil Lalonde 2016-03-17 12:19:53 -04:00
commit d330f9b62f
345 changed files with 3742 additions and 1217 deletions

View File

@ -52,7 +52,7 @@ gem 'ember-source', '1.12.2'
gem 'barber'
gem 'babel-transpiler'
gem 'message_bus', '2.0.0.beta.4'
gem 'message_bus', '2.0.0.beta.5'
gem 'rails_multisite'
@ -66,7 +66,7 @@ gem 'aws-sdk', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '0.0.8'
gem 'email_reply_trimmer', '0.1.1'
# note: for image_optim to correctly work you need to follow
# https://github.com/toy/image_optim

View File

@ -76,7 +76,7 @@ GEM
docile (1.1.5)
domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
email_reply_trimmer (0.0.8)
email_reply_trimmer (0.1.1)
ember-data-source (1.0.0.beta.16.1)
ember-source (~> 1.8)
ember-handlebars-template (0.1.5)
@ -157,7 +157,7 @@ GEM
mail (2.6.3)
mime-types (>= 1.16, < 3)
memory_profiler (0.9.6)
message_bus (2.0.0.beta.4)
message_bus (2.0.0.beta.5)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.8.2)
@ -414,7 +414,7 @@ DEPENDENCIES
byebug
certified
discourse-qunit-rails
email_reply_trimmer (= 0.0.8)
email_reply_trimmer (= 0.1.1)
ember-rails
ember-source (= 1.12.2)
excon
@ -438,7 +438,7 @@ DEPENDENCIES
lru_redux
mail
memory_profiler
message_bus (= 2.0.0.beta.4)
message_bus (= 2.0.0.beta.5)
mime-types
minitest
mocha

View File

@ -96,3 +96,15 @@
</div>
</div>
{{#if model.rejection_message}}
<hr>
<div class="control-group">
<label>{{i18n "admin.email.incoming_emails.modal.rejection_message"}}</label>
<div class="controls">
{{textarea value=model.rejection_message}}
</div>
</div>
{{/if}}

View File

@ -15,22 +15,44 @@ export default Ember.Component.extend({
this._initFromTopicList(this.get('topicList'));
}.observes('topicList.@each'),
_initFromTopicList: function(topicList) {
_initFromTopicList(topicList) {
if (topicList !== null) {
this.set('topics', topicList.get('topics'));
this.rerender();
}
},
init: function() {
init() {
this._super();
var topicList = this.get('topicList');
const topicList = this.get('topicList');
if (topicList) {
this._initFromTopicList(topicList);
} else {
// Without a topic list, we assume it's loaded always.
this.set('loaded', true);
}
},
click(e) {
// Mobile basic-topic-list doesn't use the `topic-list-item` view so
// the event for the topic entrance is never wired up.
if (!this.site.mobileView) { return; }
let target = $(e.target);
if (target.hasClass('posts-map')) {
const topicId = target.closest('tr').attr('data-topic-id');
if (topicId) {
if (target.prop('tagName') !== 'A') {
target = target.find('a');
}
const topic = this.get('topics').findProperty('id', parseInt(topicId));
this.sendAction('postsAction', {topic, position: target.offset()});
}
return false;
}
}
});

View File

@ -1,3 +1,5 @@
import { on, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
tagName: 'select',
attributeBindings: ['tabindex'],
@ -5,16 +7,6 @@ export default Ember.Component.extend({
valueAttribute: 'id',
nameProperty: 'name',
_buildData(o) {
let result = "";
if (this.resultAttributes) {
this.resultAttributes.forEach(function(a) {
result += "data-" + a + "=\"" + o.get(a) + "\" ";
});
}
return result;
},
render(buffer) {
const nameProperty = this.get('nameProperty');
const none = this.get('none');
@ -23,27 +15,27 @@ export default Ember.Component.extend({
if (typeof none === "string") {
buffer.push('<option value="">' + I18n.t(none) + "</option>");
} else if (typeof none === "object") {
buffer.push("<option value=\"\" " + this._buildData(none) + ">" + Em.get(none, nameProperty) + "</option>");
buffer.push("<option value=\"\">" + Em.get(none, nameProperty) + "</option>");
}
let selected = this.get('value');
if (!Em.isNone(selected)) { selected = selected.toString(); }
if (this.get('content')) {
const self = this;
this.get('content').forEach(function(o) {
let val = o[self.get('valueAttribute')];
this.get('content').forEach(o => {
let val = o[this.get('valueAttribute')];
if (typeof val === "undefined") { val = o; }
if (!Em.isNone(val)) { val = val.toString(); }
const selectedText = (val === selected) ? "selected" : "";
const name = Ember.get(o, nameProperty) || o;
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + self._buildData(o) + ">" + Handlebars.Utils.escapeExpression(name) + "</option>");
const name = Handlebars.Utils.escapeExpression(Ember.get(o, nameProperty) || o);
buffer.push(`<option ${selectedText} value="${val}">${name}</option>`);
});
}
},
valueChanged: function() {
@observes('value')
valueChanged() {
const $combo = this.$(),
val = this.get('value');
@ -52,19 +44,19 @@ export default Ember.Component.extend({
} else {
$combo.select2('val', null);
}
}.observes('value'),
},
_rerenderOnChange: function() {
@observes('content.@each')
_rerenderOnChange() {
this.rerender();
}.observes('content.@each'),
},
_initializeCombo: function() {
@on('didInsertElement')
_initializeCombo() {
// Workaround for https://github.com/emberjs/ember.js/issues/9813
// Can be removed when fixed. Without it, the wrong option is selected
this.$('option').each(function(i, o) {
o.selected = !!$(o).attr('selected');
});
this.$('option').each((i, o) => o.selected = !!$(o).attr('selected'));
// observer for item names changing (optional)
if (this.get('nameChanges')) {
@ -84,10 +76,11 @@ export default Ember.Component.extend({
this.set('value', val);
});
$elem.trigger('change');
}.on('didInsertElement'),
},
_destroyDropdown: function() {
@on('willDestroyElement')
_destroyDropdown() {
this.$().select2('destroy');
}.on('willDestroyElement')
}
});

View File

@ -37,6 +37,7 @@ class Toolbar {
];
this.addButton({
trimLeading: true,
id: 'bold',
group: 'fontStyles',
shortcut: 'B',
@ -44,6 +45,7 @@ class Toolbar {
});
this.addButton({
trimLeading: true,
id: 'italic',
group: 'fontStyles',
shortcut: 'I',
@ -134,7 +136,8 @@ class Toolbar {
className: button.className || button.id,
icon: button.icon || button.id,
action: button.action || 'toolbarButton',
perform: button.perform || Ember.K
perform: button.perform || Ember.K,
trimLeading: button.trimLeading
};
if (button.sendAction) {
@ -355,19 +358,26 @@ export default Ember.Component.extend({
});
},
_getSelected() {
_getSelected(trimLeading) {
if (!this.get('ready')) { return; }
const textarea = this.$('textarea.d-editor-input')[0];
const value = textarea.value;
const start = textarea.selectionStart;
var start = textarea.selectionStart;
let end = textarea.selectionEnd;
// Windows selects the space after a word when you double click
// trim trailing spaces cause **test ** would be invalid
while (end > start && /\s/.test(value.charAt(end-1))) {
end--;
}
if (trimLeading) {
// trim leading spaces cause ** test** would be invalid
while(end > start && /\s/.test(value.charAt(start))) {
start++;
}
}
const selVal = value.substring(start, end);
const pre = value.slice(0, start);
const post = value.slice(end);
@ -487,7 +497,7 @@ export default Ember.Component.extend({
actions: {
toolbarButton(button) {
const selected = this._getSelected();
const selected = this._getSelected(button.trimLeading);
const toolbarEvent = {
selected,
applySurround: (head, tail, exampleKey) => this._applySurround(selected, head, tail, exampleKey),
@ -516,18 +526,26 @@ export default Ember.Component.extend({
const link = this.get('link');
const sel = this._lastSel;
const autoHttp = function(l){
if (l.indexOf("://") === -1) {
return "http://" + l;
} else {
return l;
}
};
if (Ember.isEmpty(link)) { return; }
const m = / "([^"]+)"/.exec(link);
if (m && m.length === 2) {
const description = m[1];
const remaining = link.replace(m[0], '');
this._addText(sel, `[${description}](${remaining})`);
this._addText(sel, `[${description}](${autoHttp(remaining)})`);
} else {
if (sel.value) {
this._addText(sel, `[${sel.value}](${link})`);
this._addText(sel, `[${sel.value}](${autoHttp(link)})`);
} else {
const desc = I18n.t('composer.link_description');
this._addText(sel, `[${desc}](${link})`);
this._addText(sel, `[${desc}](${autoHttp(link)})`);
this._selectText(sel.start + 1, desc.length);
}
}

View File

@ -1,3 +1,4 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
import Combobox from 'discourse/components/combo-box';
import { on, observes } from 'ember-addons/ember-computed-decorators';
@ -11,20 +12,26 @@ export default Combobox.extend({
const details = topic.get('details');
if (details.get('can_invite_to')) {
content.push({ id: 'invite', name: I18n.t('topic.invite_reply.title') });
content.push({ id: 'invite', icon: 'users', name: I18n.t('topic.invite_reply.title') });
}
if (topic.get('bookmarked')) {
content.push({ id: 'bookmark', name: I18n.t('bookmarked.clear_bookmarks') });
content.push({ id: 'bookmark', icon: 'bookmark', name: I18n.t('bookmarked.clear_bookmarks') });
} else {
content.push({ id: 'bookmark', name: I18n.t('bookmarked.title') });
content.push({ id: 'bookmark', icon: 'bookmark', name: I18n.t('bookmarked.title') });
}
content.push({ id: 'share', name: I18n.t('topic.share.title') });
content.push({ id: 'share', icon: 'link', name: I18n.t('topic.share.title') });
if (details.get('can_flag_topic')) {
content.push({ id: 'flag', name: I18n.t('topic.flag_topic.title') });
content.push({ id: 'flag', icon: 'flag', name: I18n.t('topic.flag_topic.title') });
}
this.comboTemplate = (item) => {
const contentItem = content.findProperty('id', item.id);
if (!contentItem) { return item.text; }
return `${iconHTML(contentItem.icon)}&nbsp; ${item.text}`;
};
this.set('content', content);
},

View File

@ -30,7 +30,7 @@ export default Ember.Component.extend({
return this.get('order') === "op_likes";
}.property('order'),
click: function(e){
click(e) {
var self = this;
var on = function(sel, callback){
var target = $(e.target).closest(sel);

View File

@ -5,8 +5,6 @@ export default Ember.Component.extend({
layoutName: fmt('field.field_type', 'components/user-fields/%@'),
noneLabel: function() {
if (!this.get('field.required')) {
return 'user_fields.none';
}
}.property('field.required')
return 'user_fields.none';
}.property()
});

View File

@ -141,7 +141,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
fetchUserDetails() {
if (Discourse.User.currentProp('staff') && this.get('model.username')) {
const AdminUser = require('admin/models/admin-user').default;
AdminUser.find(this.get('model.id')).then(user => this.set('userDetails', user));
AdminUser.find(this.get('model.user_id')).then(user => this.set('userDetails', user));
}
}

View File

@ -4,6 +4,11 @@ var Tab = Em.Object.extend({
@computed('name')
location(name) {
return 'group.' + name;
},
@computed('name')
message(name) {
return I18n.t('groups.' + name);
}
});

View File

@ -29,6 +29,25 @@ export default Ember.Controller.extend(ModalFunctionality, {
Discourse.Post.showRevision(postId, postVersion).then(() => this.refresh(postId, postVersion));
},
revert(post, postVersion) {
post.revertToRevision(postVersion).then((result) => {
this.refresh(post.get('id'), postVersion);
if (result.topic) {
post.set('topic.slug', result.topic.slug);
post.set('topic.title', result.topic.title);
post.set('topic.fancy_title', result.topic.fancy_title);
}
if (result.category_id) {
post.set('topic.category', Discourse.Category.findById(result.category_id));
}
this.send("closeModal");
}).catch(function(e) {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0]) {
bootbox.alert(e.jqXHR.responseJSON.errors[0]);
}
});
},
@computed('model.created_at')
createdAtDate(createdAt) {
return moment(createdAt).format("LLLL");
@ -69,6 +88,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
return !prevHidden && this.currentUser && this.currentUser.get('staff');
},
@computed()
displayRevert() {
return this.currentUser && this.currentUser.get('staff');
},
isEitherRevisionHidden: Ember.computed.or("model.previous_hidden", "model.current_hidden"),
@computed('model.previous_hidden', 'model.current_hidden', 'displayingInline')
@ -142,6 +166,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
hideVersion() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); },
showVersion() { this.show(this.get("model.post_id"), this.get("model.current_revision")); },
revertToVersion() { this.revert(this.get("post"), this.get("model.current_revision")); },
displayInline() { this.set("viewMode", "inline"); },
displaySideBySide() { this.set("viewMode", "side_by_side"); },
displaySideBySideMarkdown() { this.set("viewMode", "side_by_side_markdown"); }

View File

@ -170,7 +170,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
userInvitedController.set('totalInvites', invite_model.invites.length);
});
} else if (this.get('isMessage') && result && result.user) {
this.get('model.details.allowed_users').pushObject(result.user);
this.get('model.details.allowed_users').pushObject(Ember.Object.create(result.user));
}
}).catch(function(e) {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {

View File

@ -4,7 +4,7 @@
**/
Discourse.Dialect.inlineRegexp({
start: '#',
matcher: /^#([\w-:]{1,50})/i,
matcher: /^#([\w-:]{1,101})/i,
spaceOrTagBoundary: true,
emitter: function(matches) {

View File

@ -110,7 +110,7 @@ function buildConnectorCache() {
_connectorCache[outletName].removeObject(viewClass);
} else {
if (!/\.raw$/.test(uniqueName)) {
viewClass = Em.View.extend({ classNames: [outletName + '-outlet', uniqueName] });
viewClass = Ember.View.extend({ classNames: [outletName + '-outlet', uniqueName] });
}
}
@ -172,8 +172,11 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options,
// just shove it in.
const viewClass = (childViews.length > 1) ? Ember.ContainerView : childViews[0];
const newHash = $.extend({}, viewInjections(env.data.view.container));
if (hash.tagName) { newHash.tagName = hash.tagName; }
delete options.fn; // we don't need the default template since we have a connector
env.helpers.view.helperFunction.call(this, [viewClass], viewInjections(env.data.view.container), options, env);
env.helpers.view.helperFunction.call(this, [viewClass], newHash, options, env);
const cvs = env.data.view._childViews;
if (childViews.length > 1 && cvs && cvs.length) {

View File

@ -9,9 +9,10 @@ export default {
const siteSettings = container.lookup('site-settings:main');
const messageBus = container.lookup('message-bus:main');
const keyValueStore = container.lookup('key-value-store:main');
const currentUser = container.lookup('current-user:main');
LogsNotice.reopenClass(Singleton, {
createCurrent() {
return this.create({ messageBus, keyValueStore, siteSettings});
return this.create({ messageBus, keyValueStore, siteSettings, currentUser });
}
});
}

View File

@ -12,7 +12,20 @@ export default {
siteSettings = container.lookup('site-settings:main');
messageBus.alwaysLongPoll = Discourse.Environment === "development";
messageBus.start();
// we do not want to start anything till document is complete
messageBus.stop();
// jQuery ready is called on "interactive" we want "complete"
// Possibly change to document.addEventListener('readystatechange',...
// but would only stop a handful of interval, message bus being delayed by
// 500ms on load is fine. stuff that needs to catch up correctly should
// pass in a position
const interval = setInterval(()=>{
if (document.readyState === "complete") {
clearInterval(interval);
messageBus.start();
}
},500);
messageBus.callbackInterval = siteSettings.anon_polling_interval;
messageBus.backgroundCallbackInterval = siteSettings.background_polling_interval;

View File

@ -169,7 +169,15 @@ Discourse.Dialect.addPreProcessor(function(text) {
var m;
while ((m = _unicodeRegexp.exec(text)) !== null) {
text = text.replace(m[0], ":" + _unicodeReplacements[m[0]] + ":");
var replacement = ":" + _unicodeReplacements[m[0]] + ":";
var before = text.charAt(m.index-1);
if (!/\B/.test(before)) {
replacement = "\u200b" + replacement;
}
text = text.replace(m[0], replacement);
}
}

View File

@ -9,6 +9,7 @@ export default function highlightSyntax($elem) {
if (!path) { return; }
$(selector, $elem).each(function(i, e) {
$(e).removeClass('lang-auto');
loadScript(path).then(() => hljs.highlightBlock(e));
});
}

View File

@ -5,7 +5,7 @@ import { addButton } from 'discourse/widgets/post-menu';
import { includeAttributes } from 'discourse/lib/transform-post';
import { addToolbarCallback } from 'discourse/components/d-editor';
import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
import { decorateWidget, changeSetting } from 'discourse/widgets/widget';
import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/widget';
import { onPageChange } from 'discourse/lib/page-tracker';
import { preventCloak } from 'discourse/widgets/post-stream';
@ -89,6 +89,8 @@ class PluginApi {
const src = Discourse.Emoji.urlFor(emoji);
return dec.h('img', { className: 'emoji', attributes: { src } });
});
iconBody = result.emoji.split('|').map(name => dec.attach('emoji', { name }));
}
if (result.text) {
@ -268,11 +270,20 @@ class PluginApi {
preventCloak(postId) {
preventCloak(postId);
}
/**
* Exposes the widget creating ability to plugins. Plugins can
* register their own plugins and attach them with decorators.
* See `createWidget` in `discourse/widgets/widget` for more info.
**/
createWidget(name, args) {
return createWidget(name, args);
}
}
let _pluginv01;
function getPluginApi(version) {
if (version === "0.1") {
if (version === "0.1" || version === "0.2") {
if (!_pluginv01) {
_pluginv01 = new PluginApi(version, Discourse.__container__);
}

View File

@ -11,9 +11,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
return _jumpScheduled;
},
/**
Jumps to a particular post in the stream
**/
// Jumps to a particular post in the stream
jumpToPost(postNumber, opts) {
const holderId = `#post_${postNumber}`;
const offset = () => {

View File

@ -286,6 +286,10 @@ Category.reopenClass({
return Discourse.ajax(`/c/${id}/show.json`);
},
reloadBySlug(slug, parentSlug) {
return parentSlug ? Discourse.ajax(`/c/${parentSlug}/${slug}/find_by_slug.json`) : Discourse.ajax(`/c/${slug}/find_by_slug.json`);
},
search(term, opts) {
var limit = 5;

View File

@ -130,6 +130,7 @@ const Group = Discourse.Model.extend({
return Discourse.ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => {
return posts.map(p => {
p.user = Discourse.User.create(p.user);
p.topic = Discourse.Topic.create(p.topic);
return Em.Object.create(p);
});
});

View File

@ -271,6 +271,10 @@ const Post = RestModel.extend({
json = Post.munge(json);
this.set('actions_summary', json.actions_summary);
}
},
revertToRevision(version) {
return Discourse.ajax(`/posts/${this.get('id')}/revisions/${version}/revert`, { type: 'PUT' });
}
});

View File

@ -2,6 +2,8 @@ import { filterQueryParams, findTopicList } from 'discourse/routes/build-topic-r
import { queryParams } from 'discourse/controllers/discovery-sortable';
import TopicList from 'discourse/models/topic-list';
import PermissionType from 'discourse/models/permission-type';
import CategoryList from 'discourse/models/category-list';
import Category from 'discourse/models/category';
// A helper function to create a category route with parameters
export default (filter, params) => {
@ -9,7 +11,19 @@ export default (filter, params) => {
queryParams,
model(modelParams) {
return { category: Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug) };
const category = Category.findBySlug(modelParams.slug, modelParams.parentSlug);
if (!category) {
return Category.reloadBySlug(modelParams.slug, modelParams.parentSlug).then((atts) => {
if (modelParams.parentSlug) {
atts.category.parentCategory = Category.findBySlug(modelParams.parentSlug);
}
const record = this.store.createRecord('category', atts.category);
record.setupGroupsAndPermissions();
this.site.updateCategory(record);
return { category: Category.findBySlug(modelParams.slug, modelParams.parentSlug) };
});
};
return { category };
},
afterModel(model, transition) {
@ -38,7 +52,6 @@ export default (filter, params) => {
_createSubcategoryList(category) {
this._categoryList = null;
if (Em.isNone(category.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
const CategoryList = require('discourse/models/category-list').default;
return CategoryList.listForParent(this.store, category).then(list => this._categoryList = list);
}

View File

@ -73,6 +73,7 @@ const TopicRoute = Discourse.Route.extend({
showHistory(model) {
showModal('history', { model });
this.controllerFor('history').refresh(model.get("id"), "latest");
this.controllerFor('history').set('post', model);
this.controllerFor('modal').set('modalClass', 'history-modal');
},

View File

@ -1,15 +1,13 @@
{{#conditional-loading-spinner condition=loading}}
{{#if topics}}
{{topic-list
showParticipants=showParticipants
showPosters=showPosters
hideCategory=hideCategory
topics=topics
expandExcerpts=expandExcerpts
bulkSelectEnabled=bulkSelectEnabled
canBulkSelect=canBulkSelect
selected=selected
}}
{{topic-list showParticipants=showParticipants
showPosters=showPosters
hideCategory=hideCategory
topics=topics
expandExcerpts=expandExcerpts
bulkSelectEnabled=bulkSelectEnabled
canBulkSelect=canBulkSelect
selected=selected}}
{{else}}
{{#unless loadingMore}}
<div class='alert alert-info'>

View File

@ -3,7 +3,7 @@
<a href="{{unbound post.user.userUrl}}" data-user-card="{{unbound post.user.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{format-date post.created_at leaveAgo="true"}}</span>
<span class="title">
<a href="{{unbound post.url}}">{{unbound post.title}}</a>
{{topic-link post.topic}}
</span>
<span class="category">{{category-link post.category}}</span>
<div class="group-member-info">

View File

@ -1,19 +1,18 @@
{{#unless skipHeader}}
<thead>
{{raw "topic-list-header"
currentUser=currentUser
canBulkSelect=canBulkSelect
toggleInTitle=toggleInTitle
hideCategory=hideCategory
showPosters=showPosters
showLikes=showLikes
showOpLikes=showOpLikes
showParticipants=showParticipants
order=order
ascending=ascending
sortable=sortable
bulkSelectEnabled=bulkSelectEnabled}}
</thead>
<thead>
{{raw "topic-list-header" currentUser=currentUser
canBulkSelect=canBulkSelect
toggleInTitle=toggleInTitle
hideCategory=hideCategory
showPosters=showPosters
showLikes=showLikes
showOpLikes=showOpLikes
showParticipants=showParticipants
order=order
ascending=ascending
sortable=sortable
bulkSelectEnabled=bulkSelectEnabled}}
</thead>
{{/unless}}
<tbody>
{{each topic in topics itemView="topic-list-item"}}

View File

@ -4,8 +4,8 @@
<ul class='action-list nav-stacked'>
{{#each tabs as |tab|}}
<li class="{{if tab.active 'active'}}">
{{#link-to tab.location model}}
{{tab.name}}
{{#link-to tab.location model title=tab.message}}
{{tab.message}}
{{#if tab.count}}<span class='count'>({{tab.count}})</span>{{/if}}
{{/link-to}}
</li>

View File

@ -1,7 +1,7 @@
{{#if controller.bulkSelectEnabled}}
<td class='star'>
<input type="checkbox" class="bulk-select">
</td>
<td class='star'>
<input type="checkbox" class="bulk-select">
</td>
{{/if}}
<td class='main-link clearfix' colspan="{{titleColSpan}}">
@ -36,18 +36,18 @@
{{#if controller.showLikes}}
<td class="num likes">
{{#if hasLikes}}
<a href='{{topic.summaryUrl}}'>
{{number topic.like_count}} <i class='fa fa-heart'></i></td>
</a>
<a href='{{topic.summaryUrl}}'>
{{number topic.like_count}} <i class='fa fa-heart'></i></td>
</a>
{{/if}}
{{/if}}
{{#if controller.showOpLikes}}
<td class="num likes">
{{#if hasOpLikes}}
<a href='{{topic.summaryUrl}}'>
{{number topic.op_like_count}} <i class='fa fa-heart'></i></td>
</a>
<a href='{{topic.summaryUrl}}'>
{{number topic.op_like_count}} <i class='fa fa-heart'></i></td>
</a>
{{/if}}
{{/if}}

View File

@ -3,7 +3,7 @@
<table class="topic-list">
<tbody>
{{#each t in topics}}
<tr {{bind-attr class="t.archived"}}>
<tr {{bind-attr class="t.archived"}} data-topic-id={{t.id}}>
<td>
<div class='main-link'>
{{topic-status topic=t}}

View File

@ -10,12 +10,6 @@
</div>
{{d-button action="loadNextVersion" icon="forward" title="post.revisions.controls.next" disabled=loadNextDisabled}}
{{d-button action="loadLastVersion" icon="fast-forward" title="post.revisions.controls.last" disabled=loadLastDisabled}}
{{#if displayHide}}
{{d-button action="hideVersion" icon="trash-o" title="post.revisions.controls.hide" class="btn-danger" disabled=loading}}
{{/if}}
{{#if displayShow}}
{{d-button action="showVersion" icon="undo" title="post.revisions.controls.show" disabled=loading}}
{{/if}}
</div>
<div id="display-modes">
{{d-button action="displayInline" label="post.revisions.displays.inline.button" title="post.revisions.displays.inline.title" class=inlineClass}}
@ -85,5 +79,15 @@
<div class="row">
{{{bodyDiff}}}
</div>
{{#if displayRevert}}
{{d-button action="revertToVersion" icon="undo" label="post.revisions.controls.revert" class="btn-danger" disabled=loading}}
{{/if}}
{{#if displayHide}}
{{d-button action="hideVersion" icon="eye-slash" label="post.revisions.controls.hide" class="btn-danger" disabled=loading}}
{{/if}}
{{#if displayShow}}
{{d-button action="showVersion" icon="eye" label="post.revisions.controls.show" disabled=loading}}
{{/if}}
</div>
</div>

View File

@ -137,13 +137,12 @@
<h3>{{{view.suggestedTitle}}}</h3>
<div class="topics">
{{#if model.isPrivateMessage}}
{{basic-topic-list
hideCategory="true"
showPosters="true"
topics=model.details.suggested_topics
postsAction="showTopicEntrance"}}
{{basic-topic-list hideCategory="true"
showPosters="true"
topics=model.details.suggested_topics
postsAction="showTopicEntrance"}}
{{else}}
{{basic-topic-list topics=model.details.suggested_topics postsAction="showTopicEntrance"}}
{{basic-topic-list topics=model.details.suggested_topics postsAction="showTopicEntrance"}}
{{/if}}
</div>
<h3>{{{view.browseMoreMessage}}}</h3>

View File

@ -183,7 +183,9 @@
{{preference-checkbox labelKey="user.email_in_reply_to" checked=model.user_option.email_in_reply_to}}
{{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}}
{{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}}
<span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}}</span>
{{#unless siteSettings.disable_mailing_list_mode}}
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}}
{{/unless}}
{{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}}
{{#unless model.user_option.email_always}}
<div class='instructions'>

View File

@ -46,11 +46,14 @@
{{#if currentUser.staff}}
<li><a href={{model.adminPath}} class="btn">{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
{{/if}}
{{plugin-outlet "user-profile-controls" tagName="li"}}
{{#if collapsedInfo}}
{{#if viewingSelf}}
<li><a {{action "expandProfile"}} href class="btn">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li>
{{/if}}
{{/if}}
</ul>
</section>

View File

@ -22,6 +22,20 @@ class DecoratorHelper {
**/
// h() is attached via `prototype` below
/**
* Attach another widget inside this one.
*
* ```
* return helper.attach('widget-name');
* ```
*/
attach(name, attrs, state) {
attrs = attrs || this.widget.attrs;
state = state || this.widget.state;
return this.widget.attach(name, attrs, state);
}
/**
* Returns the model associated with this widget. When decorating
* posts this will normally be the post.

View File

@ -0,0 +1,9 @@
import { createWidget } from 'discourse/widgets/widget';
export default createWidget('emoji', {
tagName: 'img.emoji',
buildAttributes(attrs) {
return { src: Discourse.Emoji.urlFor(attrs.name) };
},
});

View File

@ -21,27 +21,28 @@ export default createWidget('post-gutter', {
const seenTitles = {};
let i = 0;
while (i < links.length && result.length < toShow) {
const l = links[i++];
let titleCount = 0;
links.forEach(function(l) {
let title = l.title;
if (title && !seenTitles[title]) {
seenTitles[title] = true;
const linkBody = [new RawHtml({ html: `<span>${Discourse.Emoji.unescape(title)}</span>` })];
if (l.clicks) {
linkBody.push(h('span.badge.badge-notification.clicks', l.clicks.toString()));
}
titleCount++;
if (result.length < toShow) {
const linkBody = [new RawHtml({html: `<span>${Discourse.Emoji.unescape(title)}</span>`})];
if (l.clicks) {
linkBody.push(h('span.badge.badge-notification.clicks', l.clicks.toString()));
}
const className = l.reflection ? 'inbound' : 'outbound';
const link = h('a.track-link', { className, attributes: { href: l.url } }, linkBody);
result.push(h('li', link));
const className = l.reflection ? 'inbound' : 'outbound';
const link = h('a.track-link', {className, attributes: {href: l.url}}, linkBody);
result.push(h('li', link));
}
}
}
});
if (state.collapsed) {
const remaining = links.length - MAX_GUTTER_LINKS;
const remaining = titleCount - MAX_GUTTER_LINKS;
if (remaining > 0) {
result.push(h('li', h('a.toggle-more', I18n.t('post.more_links', {count: remaining}))));
}

View File

@ -348,12 +348,20 @@ createWidget('post-article', {
return rows;
},
_getTopicUrl() {
const post = this.findAncestorModel();
return post ? post.get('topic.url') : null;
},
toggleReplyAbove() {
const replyPostNumber = this.attrs.reply_to_post_number;
// jump directly on mobile
if (this.attrs.mobileView) {
DiscourseURL.jumpToPost(replyPostNumber);
const topicUrl = this._getTopicUrl();
if (topicUrl) {
DiscourseURL.routeTo(`${topicUrl}/${replyPostNumber}`);
}
return Ember.RSVP.Promise.resolve();
}
@ -361,8 +369,7 @@ createWidget('post-article', {
this.state.repliesAbove = [];
return Ember.RSVP.Promise.resolve();
} else {
const post = this.findAncestorModel();
const topicUrl = post ? post.get('topic.url') : null;
const topicUrl = this._getTopicUrl();
return this.store.find('post-reply-history', { postId: this.attrs.id }).then(posts => {
this.state.repliesAbove = posts.map((p) => {
p.shareUrl = `${topicUrl}/${p.post_number}`;

View File

@ -56,15 +56,6 @@ export default createWidget('poster-name', {
contents.push(h('span.user-title', titleContents));
}
// const cfs = attrs.userCustomFields;
// if (cfs) {
// _callbacks.forEach(cb => {
// const result = cb(cfs, attrs);
// if (result) {
//
// }
// });
// }
return contents;
}
});

View File

@ -185,8 +185,8 @@ export default createWidget('topic-map', {
tagName: 'div.topic-map',
buildKey: attrs => `topic-map-${attrs.id}`,
defaultState() {
return { collapsed: true };
defaultState(attrs) {
return { collapsed: !attrs.hasTopicSummary };
},
html(attrs, state) {

View File

@ -140,7 +140,7 @@ export default class Widget {
if (prev && prev.state) {
this.state = prev.state;
} else {
this.state = this.defaultState();
this.state = this.defaultState(this.attrs, this.state);
}
// Sometimes we pass state down from the parent

View File

@ -11,16 +11,12 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
}
.hljs-comment,
.hljs-template_comment,
.diff .hljs-header,
.hljs-javadoc {
.hljs-doctag {
color: dark-light-choose(#998, #bba);
font-style: italic;
}
.hljs-keyword,
.css .rule .hljs-keyword,
.hljs-winutils,
.javascript .hljs-title,
.nginx .hljs-title,
.hljs-subst,
@ -31,22 +27,20 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
}
.hljs-number,
.hljs-hexcolor,
.ruby .hljs-constant {
color: dark-light-choose(#099, #aff);
}
.hljs-string,
.hljs-tag .hljs-value,
.hljs-phpdoc,
.hljs-tag .hljs-string,
.tex .hljs-formula {
color: dark-light-choose(#d14, #f99);
}
.hljs-title,
.hljs-id,
.hljs-name,
.coffeescript .hljs-params,
.scss .hljs-preprocessor {
.scss .hljs-meta {
color: #900;
font-weight: bold;
}
@ -68,13 +62,13 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
.hljs-tag,
.hljs-tag .hljs-title,
.hljs-rules .hljs-property,
.django .hljs-tag .hljs-keyword {
color: dark-light-choose(#000080, #99f);
font-weight: normal;
}
.hljs-attribute,
.css .hljs-keyword,
.hljs-variable,
.lisp .hljs-body {
color: dark-light-choose(#008080, #0ee);
@ -94,16 +88,12 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
.hljs-built_in,
.lisp .hljs-title,
.clojure .hljs-built_in {
.clojure .hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-preprocessor,
.hljs-pragma,
.hljs-pi,
.hljs-doctype,
.hljs-shebang,
.hljs-cdata {
.meta {
color: #999;
font-weight: bold;
}
@ -116,11 +106,7 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
background: #dfd;
}
.diff .hljs-change {
background: #0086b3;
}
.hljs-chunk {
.diff .hljs-meta {
color: #aaa;
}

View File

@ -260,6 +260,7 @@
#bulk-select {
position: fixed;
right: 20px;
top: 130px;
padding: 5px;
background-color: $secondary;
z-index: 99999;

View File

@ -100,7 +100,6 @@ button {
bottom: 0;
left: 135px;
z-index: 1000;
display: none;
h3 {
margin-top: 0;

View File

@ -131,7 +131,7 @@ dfn {
*/
h1 {
font-size: 2em;
font-size: 1.6em;
margin: 0.67em 0;
}
@ -422,4 +422,4 @@ table {
td,
th {
padding: 0;
}
}

View File

@ -1,12 +1,22 @@
require_dependency 'rate_limiter'
class AboutController < ApplicationController
skip_before_filter :check_xhr, only: [:show]
skip_before_filter :check_xhr, only: [:index]
before_filter :ensure_logged_in, only: [:live_post_counts]
def index
@about = About.new
render_serialized(@about, AboutSerializer)
respond_to do |format|
format.html do
# @list = list
# store_preloaded(list.preload_key, MultiJson.dump(TopicListSerializer.new(list, scope: guardian)))
render :index
end
format.json do
render_serialized(@about, AboutSerializer)
end
end
end
def live_post_counts

View File

@ -47,7 +47,7 @@ class Admin::EmailController < Admin::AdminController
def handle_mail
params.require(:email)
Email::Receiver.new(params[:email]).process
Email::Receiver.new(params[:email]).process!
render text: "email was processed"
end

View File

@ -20,7 +20,8 @@ class Admin::EmailTemplatesController < Admin::AdminController
"system_messages.unblocked", "system_messages.user_automatically_blocked",
"system_messages.welcome_invite", "system_messages.welcome_user", "test_mailer",
"user_notifications.account_created", "user_notifications.admin_login",
"user_notifications.authorize_email", "user_notifications.forgot_password",
"user_notifications.confirm_new_email", "user_notifications.confirm_old_email",
"user_notifications.notify_old_email", "user_notifications.forgot_password",
"user_notifications.set_password", "user_notifications.signup",
"user_notifications.signup_after_approval",
"user_notifications.user_invited_to_private_message_pm",

View File

@ -8,7 +8,7 @@ class Admin::UserFieldsController < Admin::AdminController
field = UserField.new(params.require(:user_field).permit(*Admin::UserFieldsController.columns))
field.position = (UserField.maximum(:position) || 0) + 1
field.required = params[:required] == "true"
field.required = params[:user_field][:required] == "true"
update_options(field)
json_result(field, serializer: UserFieldSerializer) do

View File

@ -75,9 +75,13 @@ class ApplicationController < ActionController::Base
render 'default/empty'
end
def render_rate_limit_error(e)
render_json_error e.description, type: :rate_limit, status: 429
end
# If they hit the rate limiter
rescue_from RateLimiter::LimitExceeded do |e|
render_json_error e.description, type: :rate_limit, status: 429
render_rate_limit_error(e)
end
rescue_from PG::ReadOnlySqlTransaction do |e|
@ -165,7 +169,7 @@ class ApplicationController < ActionController::Base
def set_locale
if !current_user
if SiteSetting.allow_user_locale
if SiteSetting.set_locale_from_accept_language_header
I18n.locale = locale_from_header
else
I18n.locale = SiteSetting.default_locale

View File

@ -2,7 +2,7 @@ require_dependency 'category_serializer'
class CategoriesController < ApplicationController
before_filter :ensure_logged_in, except: [:index, :show, :redirect]
before_filter :ensure_logged_in, except: [:index, :show, :redirect, :find_by_slug]
before_filter :fetch_category, only: [:show, :update, :destroy]
before_filter :initialize_staff_action_logger, only: [:create, :update, :destroy]
skip_before_filter :check_xhr, only: [:index, :redirect]
@ -102,19 +102,16 @@ class CategoriesController < ApplicationController
json_result(@category, serializer: CategorySerializer) do |cat|
cat.move_to(category_params[:position].to_i) if category_params[:position]
category_params.delete(:position)
if category_params.key? :email_in and category_params[:email_in].length == 0
# properly null the value so the database constrain doesn't catch us
# properly null the value so the database constraint doesn't catch us
if category_params.has_key?(:email_in) && category_params[:email_in].blank?
category_params[:email_in] = nil
elsif category_params.key? :email_in and existing_category = Category.find_by(email_in: category_params[:email_in]) and existing_category.id != @category.id
# check if email_in address is already in use for other category
return render_json_error I18n.t('category.errors.email_in_already_exist', {email_in: category_params[:email_in], category_name: existing_category.name})
end
category_params.delete(:position)
old_permissions = Category.find(@category.id).permissions_params
old_permissions = cat.permissions_params
if result = cat.update_attributes(category_params)
if result = cat.update(category_params)
Scheduler::Defer.later "Log staff action change category settings" do
@staff_action_logger.log_category_settings_change(@category, category_params, old_permissions)
end
@ -156,6 +153,15 @@ class CategoriesController < ApplicationController
render json: success_json
end
def find_by_slug
params.require(:category_slug)
@category = Category.find_by_slug(params[:category_slug], params[:parent_category_slug])
guardian.ensure_can_see!(@category)
@category.permission = CategoryGroup.permission_types[:full] if Category.topic_create_allowed(guardian).where(id: @category.id).exists?
render_serialized(@category, CategorySerializer)
end
private
def required_param_keys

View File

@ -23,6 +23,15 @@ class PostActionsController < ApplicationController
@post.reload
render_post_json(@post, _add_raw = false)
end
rescue RateLimiter::LimitExceeded => e
# Special case: if we hit the create like rate limit, record it in user history
# so we can award a badge
if e.type == "create_like"
UserHistory.create!(action: UserHistory.actions[:rate_limited_like],
target_user_id: current_user.id,
post_id: @post.id)
end
render_rate_limit_error(e)
end
def destroy

View File

@ -282,6 +282,55 @@ class PostsController < ApplicationController
render nothing: true
end
def revert
raise Discourse::NotFound unless guardian.is_staff?
post_id = params[:id] || params[:post_id]
revision = params[:revision].to_i
raise Discourse::InvalidParameters.new(:revision) if revision < 2
post_revision = PostRevision.find_by(post_id: post_id, number: revision)
raise Discourse::NotFound unless post_revision
post = find_post_from_params
raise Discourse::NotFound if post.blank?
post_revision.post = post
guardian.ensure_can_see!(post_revision)
guardian.ensure_can_edit!(post)
return render_json_error(I18n.t('revert_version_same')) if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? && post_revision.modifications["category_id"].blank?
topic = Topic.with_deleted.find(post.topic_id)
changes = {}
changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications["raw"].present? && post_revision.modifications["raw"][0] != post.raw
if post.is_first_post?
changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications["title"].present? && post_revision.modifications["title"][0] != topic.title
changes[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? && post_revision.modifications["category_id"][0] != topic.category.id
end
return render_json_error(I18n.t('revert_version_same')) unless changes.length > 0
changes[:edit_reason] = "reverted to version ##{post_revision.number.to_i - 1}"
revisor = PostRevisor.new(post, topic)
revisor.revise!(current_user, changes)
return render_json_error(post) if post.errors.present?
return render_json_error(topic) if topic.errors.present?
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)
link_counts = TopicLink.counts_for(guardian, topic, [post])
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
result = { post: post_serializer.as_json }
if post.is_first_post?
result[:topic] = BasicTopicSerializer.new(topic, scope: guardian, root: false).as_json if post_revision.modifications["title"].present?
result[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present?
end
render_json_dump(result)
end
def bookmark
post = find_post_from_params

View File

@ -5,9 +5,9 @@ require_dependency 'rate_limiter'
class UsersController < ApplicationController
skip_before_filter :authorize_mini_profiler, only: [:avatar]
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :authorize_email, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login]
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login]
before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails]
before_filter :ensure_logged_in, only: [:username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails]
before_filter :respond_to_suspicious_request, only: [:create]
# we need to allow account creation with bad CSRF tokens, if people are caching, the CSRF token on the
@ -21,7 +21,6 @@ class UsersController < ApplicationController
:activate_account,
:perform_account_activation,
:send_activation_email,
:authorize_email,
:password_reset,
:confirm_email_token,
:admin_login]
@ -471,45 +470,6 @@ class UsersController < ApplicationController
end
end
def change_email
params.require(:email)
user = fetch_user_from_params
guardian.ensure_can_edit_email!(user)
lower_email = Email.downcase(params[:email]).strip
RateLimiter.new(user, "change-email-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(user, "change-email-min-#{request.remote_ip}", 3, 1.minute).performed!
EmailValidator.new(attributes: :email).validate_each(user, :email, lower_email)
return render_json_error(user.errors.full_messages) if user.errors[:email].present?
# Raise an error if the email is already in use
return render_json_error(I18n.t('change_email.error')) if User.find_by_email(lower_email)
email_token = user.email_tokens.create(email: lower_email)
Jobs.enqueue(
:user_email,
to_address: lower_email,
type: :authorize_email,
user_id: user.id,
email_token: email_token.token
)
render nothing: true
rescue RateLimiter::LimitExceeded
render_json_error(I18n.t("rate_limiter.slow_down"))
end
def authorize_email
expires_now()
if @user = EmailToken.confirm(params[:token])
log_on_user(@user)
else
flash[:error] = I18n.t('change_email.error')
end
render layout: 'no_ember'
end
def account_created
@message = session['user_created_message'] || I18n.t('activation.missing_session')
expires_now

View File

@ -0,0 +1,46 @@
require_dependency 'rate_limiter'
require_dependency 'email_validator'
require_dependency 'email_updater'
class UsersEmailController < ApplicationController
before_filter :ensure_logged_in, only: [:index, :update]
skip_before_filter :check_xhr, only: [:confirm]
skip_before_filter :redirect_to_login_if_required, only: [:confirm]
def index
end
def update
params.require(:email)
user = fetch_user_from_params
RateLimiter.new(user, "change-email-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(user, "change-email-min-#{request.remote_ip}", 3, 1.minute).performed!
updater = EmailUpdater.new(guardian, user)
updater.change_to(params[:email])
if updater.errors.present?
return render_json_error(updater.errors.full_messages)
end
render nothing: true
rescue RateLimiter::LimitExceeded
render_json_error(I18n.t("rate_limiter.slow_down"))
end
def confirm
expires_now
updater = EmailUpdater.new
@update_result = updater.confirm(params[:token])
# Log in the user if the process is complete (and they're not logged in)
log_on_user(updater.user) if @update_result == :complete
render layout: 'no_ember'
end
end

View File

@ -169,6 +169,10 @@ module ApplicationHelper
MobileDetection.resolve_mobile_view!(request.user_agent,params,session)
end
def include_crawler_content?
controller.try(:use_crawler_layout?) || !mobile_view?
end
def mobile_device?
MobileDetection.mobile_device?(request.user_agent)
end

View File

@ -109,6 +109,10 @@ module Jobs
email_args[:email_token] = email_token
end
if type == 'notify_old_email'
email_args[:new_email] = user.email
end
message = UserNotifications.send(type, user, email_args)
# Update the to address if we have a custom one

View File

@ -14,6 +14,7 @@ module Jobs
Topic.ensure_consistency!
Badge.ensure_consistency!
CategoryUser.ensure_consistency!
UserOption.ensure_consistency!
end
end
end

View File

@ -23,9 +23,14 @@ module Jobs
def process_popmail(popmail)
begin
mail_string = popmail.pop
Email::Receiver.new(mail_string).process
receiver = Email::Receiver.new(mail_string)
receiver.process!
rescue => e
handle_failure(mail_string, e)
rejection_message = handle_failure(mail_string, e)
if rejection_message.present? && receiver && receiver.incoming_email
receiver.incoming_email.rejection_message = rejection_message.body.to_s
receiver.incoming_email.save
end
end
end
@ -35,7 +40,6 @@ module Jobs
message_template = case e
when Email::Receiver::EmptyEmailError then :email_reject_empty
when Email::Receiver::NoBodyDetectedError then :email_reject_empty
when Email::Receiver::NoMessageIdError then :email_reject_no_message_id
when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated
when Email::Receiver::InactiveUserError then :email_reject_inactive_user
when Email::Receiver::BlockedUserError then :email_reject_blocked_user
@ -49,6 +53,7 @@ module Jobs
when ActiveRecord::Rollback then :email_reject_invalid_post
when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action
when Discourse::InvalidAccess then :email_reject_invalid_access
when RateLimiter::LimitExceeded then :email_reject_rate_limit_specified
end
template_args = {}
@ -59,6 +64,10 @@ module Jobs
template_args[:post_error] = e.message
end
if message_template == :email_reject_rate_limit_specified
template_args[:rate_limit_description] = e.description
end
if message_template
# inform the user about the rejection
message = Mail::Message.new(mail_string)
@ -68,7 +77,10 @@ module Jobs
client_message = RejectionMailer.send_rejection(message_template, message.from, template_args)
Email::Sender.new(client_message, message_template).send
client_message
else
mark_as_errored!
Discourse.handle_job_exception(e, error_context(@args, "Unrecognized error type when processing incoming email", mail: mail_string))
end
end
@ -83,8 +95,21 @@ module Jobs
end
end
rescue Net::POPAuthenticationError => e
mark_as_errored!
Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming email"))
end
POLL_MAILBOX_ERRORS_KEY ||= "poll_mailbox_errors".freeze
def self.errors_in_past_24_hours
$redis.zremrangebyscore(POLL_MAILBOX_ERRORS_KEY, 0, 24.hours.ago.to_i)
$redis.zcard(POLL_MAILBOX_ERRORS_KEY).to_i
end
def mark_as_errored!
now = Time.now.to_i
$redis.zadd(POLL_MAILBOX_ERRORS_KEY, now, now.to_s)
end
end
end

View File

@ -23,9 +23,23 @@ class UserNotifications < ActionMailer::Base
new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale))
end
def authorize_email(user, opts={})
def notify_old_email(user, opts={})
build_email(user.email,
template: "user_notifications.authorize_email",
template: "user_notifications.notify_old_email",
locale: user_locale(user),
new_email: opts[:new_email])
end
def confirm_old_email(user, opts={})
build_email(user.email,
template: "user_notifications.confirm_old_email",
locale: user_locale(user),
email_token: opts[:email_token])
end
def confirm_new_email(user, opts={})
build_email(user.email,
template: "user_notifications.confirm_new_email",
locale: user_locale(user),
email_token: opts[:email_token])
end

View File

@ -63,7 +63,7 @@ class AdminDashboardData
:send_consumer_email_check, :title_check,
:site_description_check, :site_contact_username_check,
:notification_email_check, :subfolder_ends_in_slash_check,
:pop3_polling_configuration
:pop3_polling_configuration, :email_polling_errored_recently
add_problem_check do
sidekiq_check || queue_size_check
@ -210,4 +210,9 @@ class AdminDashboardData
POP3PollingEnabledSettingValidator.new.error_message if SiteSetting.pop3_polling_enabled
end
def email_polling_errored_recently
errors = Jobs::PollMailbox.errors_in_past_24_hours
I18n.t('dashboard.email_polling_errored_recently', count: errors) if errors > 0
end
end

View File

@ -20,13 +20,27 @@ class Badge < ActiveRecord::Base
GoodShare = 22
GreatShare = 23
OneYearAnniversary = 24
Promoter = 25
Campaigner = 26
Champion = 27
PopularLink = 28
HotLink = 29
FamousLink = 30
Appreciated = 36
Respected = 37
Admired = 31
OutOfLove = 33
HigherLove = 34
CrazyInLove = 35
ThankYou = 38
GivesBack = 32
Empathetic = 39
# other consts
AutobiographerMinBioLength = 10
@ -233,7 +247,7 @@ SQL
def self.sharing_badge(count)
<<SQL
SELECT views.user_id, i2.post_id, i2.created_at granted_at
SELECT views.user_id, i2.post_id, current_timestamp granted_at
FROM
(
SELECT i.user_id, MIN(i.id) i_id
@ -249,7 +263,7 @@ SQL
def self.linking_badge(count)
<<-SQL
SELECT tl.user_id, post_id, MIN(tl.created_at) granted_at
SELECT tl.user_id, post_id, current_timestamp granted_at
FROM topic_links tl
JOIN posts p ON p.id = post_id AND p.deleted_at IS NULL
JOIN topics t ON t.id = p.topic_id AND t.deleted_at IS NULL AND t.archetype <> 'private_message'
@ -259,6 +273,40 @@ SQL
SQL
end
def self.liked_posts(post_count, like_count)
<<-SQL
SELECT p.user_id, current_timestamp AS granted_at
FROM posts AS p
WHERE p.like_count >= #{like_count}
AND (:backfill OR p.user_id IN (:user_ids))
GROUP BY p.user_id
HAVING count(*) > #{post_count}
SQL
end
def self.like_rate_limit(count)
<<-SQL
SELECT uh.target_user_id AS user_id, MAX(uh.created_at) AS granted_at
FROM user_histories AS uh
WHERE uh.action = #{UserHistory.actions[:rate_limited_like]}
AND (:backfill OR uh.target_user_id IN (:user_ids))
GROUP BY uh.target_user_id
HAVING COUNT(*) >= #{count}
SQL
end
def self.liked_back(min_posts, ratio)
<<-SQL
SELECT p.user_id, current_timestamp AS granted_at
FROM posts AS p
INNER JOIN user_stats AS us ON us.user_id = p.user_id
WHERE p.like_count > 0
AND (:backfill OR p.user_id IN (:user_ids))
GROUP BY p.user_id, us.likes_given
HAVING count(*) > #{min_posts}
AND (us.likes_given / count(*)::float) > #{ratio}
SQL
end
end
belongs_to :badge_type
@ -280,7 +328,6 @@ SQL
[:badge_type_id, :multiple_grant, :target_posts, :show_posts, :query, :trigger, :auto_revoke, :listable]
end
def self.trust_level_badge_ids
(1..4).to_a
end

View File

@ -316,8 +316,12 @@ SQL
def email_in_validator
return if self.email_in.blank?
email_in.split("|").each do |email|
unless Email.is_valid?(email)
self.errors.add(:base, I18n.t('category.errors.invalid_email_in', email_in: email))
if !Email.is_valid?(email)
self.errors.add(:base, I18n.t('category.errors.invalid_email_in', email: email))
elsif group = Group.find_by_email(email)
self.errors.add(:base, I18n.t('category.errors.email_already_used_in_group', email: email, group_name: group.name))
elsif category = Category.where.not(id: self.id).find_by_email(email)
self.errors.add(:base, I18n.t('category.errors.email_already_used_in_category', email: email, category_name: category.name))
end
end
end
@ -391,7 +395,7 @@ SQL
end
def self.find_by_email(email)
self.where("email_in LIKE ?", "%#{Email.downcase(email)}%").first
self.where("string_to_array(email_in, '|') @> ARRAY[?]", Email.downcase(email)).first
end
def has_children?
@ -446,6 +450,15 @@ SQL
def publish_discourse_stylesheet
DiscourseStylesheets.cache.clear
end
def self.find_by_slug(category_slug, parent_category_slug=nil)
if parent_category_slug
parent_category_id = self.where(slug: parent_category_slug, parent_category_id: nil).pluck(:id).first
self.where(slug: category_slug, parent_category_id: parent_category_id).first
else
self.where(slug: category_slug, parent_category_id: nil).first
end
end
end
# == Schema Information

View File

@ -91,7 +91,7 @@ class DirectoryItem < ActiveRecord::Base
UPDATE directory_items di SET
likes_received = x.likes_received,
likes_given = x.likes_given,
topics_entered = x.likes_given,
topics_entered = x.topics_entered,
days_visited = x.days_visited,
posts_read = x.posts_read,
topic_count = x.topic_count,
@ -102,7 +102,7 @@ class DirectoryItem < ActiveRecord::Base
di.period_type = :period_type AND (
di.likes_received <> x.likes_received OR
di.likes_given <> x.likes_given OR
di.topics_entered <> x.likes_given OR
di.topics_entered <> x.topics_entered OR
di.days_visited <> x.days_visited OR
di.posts_read <> x.posts_read OR
di.topic_count <> x.topic_count OR

View File

@ -0,0 +1,9 @@
class EmailChangeRequest < ActiveRecord::Base
belongs_to :old_email_token, class_name: 'EmailToken'
belongs_to :new_email_token, class_name: 'EmailToken'
def self.states
@states ||= Enum.new(authorizing_old: 1, authorizing_new: 2, complete: 3)
end
end

View File

@ -41,28 +41,41 @@ class EmailToken < ActiveRecord::Base
return token.present? && token =~ /[a-f0-9]{#{token.length/2}}/i
end
def self.confirm(token)
return unless valid_token_format?(token)
def self.atomic_confirm(token)
failure = { success: false }
return failure unless valid_token_format?(token)
email_token = confirmable(token)
return if email_token.blank?
return failure if email_token.blank?
user = email_token.user
failure[:user] = user
row_count = EmailToken.where(id: email_token.id, expired: false).update_all 'confirmed = true'
if row_count == 1
return { success: true, user: user, email_token: email_token }
end
return failure
end
def self.confirm(token)
User.transaction do
row_count = EmailToken.where(id: email_token.id, expired: false).update_all 'confirmed = true'
if row_count == 1
result = atomic_confirm(token)
user = result[:user]
if result[:success]
# If we are activating the user, send the welcome message
user.send_welcome_message = !user.active?
user.active = true
user.email = email_token.email
user.email = result[:email_token].email
user.save!
end
end
# redeem invite, if available
return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present?
user
if user
return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present?
user
end
end
rescue ActiveRecord::RecordInvalid
# If the user's email is already taken, just return nil (failure)
end

View File

@ -1,4 +1,7 @@
class Emoji
# update this to clear the cache
EMOJI_VERSION = "v2"
include ActiveModel::SerializerSupport
attr_reader :path
@ -20,19 +23,19 @@ class Emoji
end
def self.all
Discourse.cache.fetch("all_emojis:v2") { standard | custom }
Discourse.cache.fetch("all_emojis:#{EMOJI_VERSION}") { standard | custom }
end
def self.standard
Discourse.cache.fetch("standard_emojis:v2") { load_standard }
Discourse.cache.fetch("standard_emojis:#{EMOJI_VERSION}") { load_standard }
end
def self.aliases
Discourse.cache.fetch("aliases_emojis:v2") { load_aliases }
Discourse.cache.fetch("aliases_emojis:#{EMOJI_VERSION}") { load_aliases }
end
def self.custom
Discourse.cache.fetch("custom_emojis:v2") { load_custom }
Discourse.cache.fetch("custom_emojis:#{EMOJI_VERSION}") { load_custom }
end
def self.exists?(name)
@ -76,10 +79,10 @@ class Emoji
end
def self.clear_cache
Discourse.cache.delete("custom_emojis")
Discourse.cache.delete("standard_emojis")
Discourse.cache.delete("aliases_emojis")
Discourse.cache.delete("all_emojis")
Discourse.cache.delete("custom_emojis:#{EMOJI_VERSION}")
Discourse.cache.delete("standard_emojis:#{EMOJI_VERSION}")
Discourse.cache.delete("aliases_emojis:#{EMOJI_VERSION}")
Discourse.cache.delete("all_emojis:#{EMOJI_VERSION}")
end
def self.db_file
@ -128,8 +131,8 @@ class Emoji
@unicode_replacements = {}
db['emojis'].each do |e|
hex = e['code'].hex
# Don't replace digits or letters
if hex > 128
# Don't replace digits, letters and some symbols
if hex > 255 && e['name'] != 'tm'
@unicode_replacements[[hex].pack('U')] = e['name']
end
end

View File

@ -43,6 +43,8 @@ class GlobalSetting
c = {}
c[:host] = redis_host if redis_host
c[:port] = redis_port if redis_port
c[:slave_host] = redis_slave_host if redis_slave_host
c[:slave_port] = redis_slave_port if redis_slave_port
c[:password] = redis_password if redis_password.present?
c[:db] = redis_db if redis_db != 0
c[:db] = 1 if Rails.env == "test"
@ -52,6 +54,7 @@ class GlobalSetting
{host: host, port: port}
end.to_a
end
c[:connector] = DiscourseRedis::Connector
c.freeze
end
end

View File

@ -82,8 +82,12 @@ class Group < ActiveRecord::Base
def incoming_email_validator
return if self.automatic || self.incoming_email.blank?
incoming_email.split("|").each do |email|
unless Email.is_valid?(email)
self.errors.add(:base, I18n.t('groups.errors.invalid_incoming_email', incoming_email: email))
if !Email.is_valid?(email)
self.errors.add(:base, I18n.t('groups.errors.invalid_incoming_email', email: email))
elsif group = Group.where.not(id: self.id).find_by_email(email)
self.errors.add(:base, I18n.t('groups.errors.email_already_used_in_group', email: email, group_name: group.name))
elsif category = Category.find_by_email(email)
self.errors.add(:base, I18n.t('groups.errors.email_already_used_in_category', email: email, category_name: category.name))
end
end
end
@ -334,7 +338,7 @@ class Group < ActiveRecord::Base
end
def self.find_by_email(email)
self.where("incoming_email LIKE ?", "%#{Email.downcase(email)}%").first
self.where("string_to_array(incoming_email, '|') @> ARRAY[?]", Email.downcase(email)).first
end
def bulk_add(user_ids)

View File

@ -108,6 +108,7 @@ class OptimizedImage < ActiveRecord::Base
-interpolate bicubic
-unsharp 2x0.5+0.7+0
-quality 98
-profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
#{to}
}
end
@ -130,6 +131,7 @@ class OptimizedImage < ActiveRecord::Base
-gravity center
-background transparent
-resize #{dimensions}#{!!opts[:force_aspect_ratio] ? "\\!" : "\\>"}
-profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
#{to}
}
end

View File

@ -222,6 +222,10 @@ class Post < ActiveRecord::Base
@acting_user = pu
end
def last_editor
self.last_editor_id ? (User.find_by_id(self.last_editor_id) || user) : user
end
def whitelisted_spam_hosts
hosts = SiteSetting
@ -435,7 +439,12 @@ class Post < ActiveRecord::Base
new_user: new_user.username_lower
)
revise(actor, { raw: self.raw, user_id: new_user.id, edit_reason: edit_reason })
revise(actor, {raw: self.raw, user_id: new_user.id, edit_reason: edit_reason}, bypass_bump: true)
if post_number == topic.highest_post_number
topic.update_columns(last_post_user_id: new_user.id)
end
end
before_create do

View File

@ -113,6 +113,8 @@ class PostMover
def update_statistics
destination_topic.update_statistics
original_topic.update_statistics
TopicUser.update_post_action_cache(topic_id: original_topic.id, post_action_type: :bookmark)
TopicUser.update_post_action_cache(topic_id: destination_topic.id, post_action_type: :bookmark)
end
def update_user_actions

View File

@ -105,6 +105,10 @@ class SiteSetting < ActiveRecord::Base
nil
end
def self.email_polling_enabled?
SiteSetting.manual_polling_enabled? || SiteSetting.pop3_polling_enabled?
end
end
# == Schema Information

View File

@ -36,6 +36,7 @@ class User < ActiveRecord::Base
has_many :uploads
has_many :warnings
has_many :user_archived_messages, dependent: :destroy
has_many :email_change_requests, dependent: :destroy
has_one :user_option, dependent: :destroy
@ -147,6 +148,33 @@ class User < ActiveRecord::Base
User.where(username_lower: lower).blank? && !SiteSetting.reserved_usernames.split("|").include?(username)
end
def self.plugin_staff_user_custom_fields
@plugin_staff_user_custom_fields ||= {}
end
def self.register_plugin_staff_custom_field(custom_field_name, plugin)
plugin_staff_user_custom_fields[custom_field_name] = plugin
end
def self.whitelisted_user_custom_fields(guardian)
fields = []
if SiteSetting.public_user_custom_fields.present?
fields += SiteSetting.public_user_custom_fields.split('|')
end
if guardian.is_staff?
if SiteSetting.staff_user_custom_fields.present?
fields += SiteSetting.staff_user_custom_fields.split('|')
end
plugin_staff_user_custom_fields.each do |k, v|
fields << k if v.enabled?
end
end
fields.uniq
end
def effective_locale
if SiteSetting.allow_user_locale && self.locale.present?
self.locale

View File

@ -51,7 +51,8 @@ class UserHistory < ActiveRecord::Base
revoke_admin: 33,
grant_moderation: 34,
revoke_moderation: 35,
backup_operation: 36)
backup_operation: 36,
rate_limited_like: 37)
end
# Staff actions is a subset of all actions, used to audit actions taken by staff users.

View File

@ -5,6 +5,14 @@ class UserOption < ActiveRecord::Base
after_save :update_tracked_topics
def self.ensure_consistency!
exec_sql("SELECT u.id FROM users u
LEFT JOIN user_options o ON o.user_id = u.id
WHERE o.user_id IS NULL").values.each do |id,_|
UserOption.create(user_id: id.to_i)
end
end
def self.previous_replies_type
@previous_replies_type ||= Enum.new(always: 0, unless_emailed: 1, never: 2)
end
@ -44,6 +52,11 @@ class UserOption < ActiveRecord::Base
true
end
def mailing_list_mode
return false if SiteSetting.disable_mailing_list_mode
super
end
def update_tracked_topics
return unless auto_track_topics_after_msecs_changed?
TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call

View File

@ -6,6 +6,7 @@ class GroupPostSerializer < ApplicationSerializer
:url,
:user_title,
:user_long_name,
:topic,
:category
has_one :user, serializer: BasicUserSerializer, embed: :objects
@ -26,8 +27,11 @@ class GroupPostSerializer < ApplicationSerializer
SiteSetting.enable_names?
end
def topic
object.topic
end
def category
object.topic.category
end
end

View File

@ -2,6 +2,7 @@ class IncomingEmailDetailsSerializer < ApplicationSerializer
attributes :error,
:error_description,
:rejection_message,
:return_path,
:date,
:from,

View File

@ -318,15 +318,10 @@ class UserSerializer < BasicUserSerializer
end
def custom_fields
fields = nil
fields = User.whitelisted_user_custom_fields(scope)
if scope.can_edit?(object)
fields = DiscoursePluginRegistry.serialized_current_user_fields.to_a
end
if SiteSetting.public_user_custom_fields.present?
fields ||= []
fields += SiteSetting.public_user_custom_fields.split('|')
fields += DiscoursePluginRegistry.serialized_current_user_fields.to_a
end
if fields.present?

View File

@ -38,14 +38,23 @@ class PostAlerter
# mentions (users/groups)
mentioned_groups, mentioned_users = extract_mentions(post)
expand_group_mentions(mentioned_groups, post) do |group, users|
notify_users(users - notified, :group_mentioned, post, group: group)
notified += users
end
if mentioned_groups || mentioned_users
mentioned_opts = {}
if post.last_editor_id != post.user_id
# Mention comes from an edit by someone else, so notification should say who added the mention.
editor = post.last_editor
mentioned_opts = {user_id: editor.id, original_username: editor.username, display_username: editor.username}
end
if mentioned_users
notify_users(mentioned_users - notified, :mentioned, post)
notified += mentioned_users
expand_group_mentions(mentioned_groups, post) do |group, users|
notify_users(users - notified, :group_mentioned, post, mentioned_opts.merge({group: group}))
notified += users
end
if mentioned_users
notify_users(mentioned_users - notified, :mentioned, post, mentioned_opts)
notified += mentioned_users
end
end
# replies
@ -208,15 +217,19 @@ class PostAlerter
end
def should_notify_previous?(user, notification, opts)
type = notification.notification_type
if type == Notification.types[:edited]
return should_notify_edit?(notification, opts)
elsif type == Notification.types[:liked]
return should_notify_like?(user, notification)
case notification.notification_type
when Notification.types[:edited] then should_notify_edit?(notification, opts)
when Notification.types[:liked] then should_notify_like?(user, notification)
else false
end
return false
end
COLLAPSED_NOTIFICATION_TYPES ||= [
Notification.types[:replied],
Notification.types[:quoted],
Notification.types[:posted],
]
def create_notification(user, type, post, opts=nil)
return if user.blank?
return if user.id == Discourse::SYSTEM_USER_ID
@ -228,7 +241,7 @@ class PostAlerter
# Make sure the user can see the post
return unless Guardian.new(user).can_see?(post)
notifier_id = opts[:user_id] || post.user_id
notifier_id = opts[:user_id] || post.user_id # xxxxx look at revision history
# apply muting here
return if notifier_id && MutedUser.where(user_id: user.id, muted_user_id: notifier_id)
@ -268,9 +281,10 @@ class PostAlerter
collapsed = false
if type == Notification.types[:replied] || type == Notification.types[:posted]
destroy_notifications(user, Notification.types[:replied], post.topic)
destroy_notifications(user, Notification.types[:posted], post.topic)
if COLLAPSED_NOTIFICATION_TYPES.include?(type)
COLLAPSED_NOTIFICATION_TYPES.each do |t|
destroy_notifications(user, t, post.topic)
end
collapsed = true
end
@ -280,7 +294,7 @@ class PostAlerter
end
original_post = post
original_username = opts[:display_username] || post.username
original_username = opts[:display_username] || post.username # xxxxx need something here too
if collapsed
post = first_unread_post(user, post.topic) || post

View File

@ -0,0 +1,100 @@
<% content_for :title do %><%=t "about" %><% end %>
<section itemscope itemtype="https://schema.org/AboutPage">
<h1 itemprop="name">
<%=t "js.about.title", {title: @about.title} %>
</h1>
<div itemprop="text">
<%= @about.description %>
</div>
<h2><%=t "js.about.our_admins" %></h2>
<div class='admins-list' itemscope itemtype='http://schema.org/ItemList'>
<% @about.admins.each do |user| %>
<div itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
<meta itemprop='url' content='<%= user_path(user) %>'>
<a href='<%= user_path(user) %>' itemprop='item'>
<span itemprop='image'>
<img width="45" height="45" class="avatar" src="<%= user.small_avatar_url %>">
</span>
<span itemprop='name'>
<%= user.username %>
<% if user.name.present? %>
- <%= user.name %>
<% end %>
</span>
</a>
</div>
<% end %>
</div>
<% if @about.moderators.count > 0 %>
<h2><%=t "js.about.our_moderators" %></h2>
<div class='moderators-list' itemscope itemtype='http://schema.org/ItemList'>
<% @about.moderators.each do |user| %>
<div itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
<meta itemprop='url' content='<%= user_path(user) %>'>
<a href='<%= user_path(user) %>' itemprop='item'>
<span itemprop='image'>
<img width="45" height="45" class="avatar" src="<%= user.small_avatar_url %>">
</span>
<span itemprop='name'>
<%= user.username %>
<% if user.name.present? %>
- <%= user.name %>
<% end %>
</span>
</a>
</div>
<% end %>
</div>
<% end %>
<section class='about stats'>
<h2><%=t 'js.about.stats' %></h2>
<table class='table'>
<tr>
<th>&nbsp;</th>
<th><%=t 'js.about.stat.all_time' %></th>
<th><%=t 'js.about.stat.last_7_days' %></th>
<th><%=t 'js.about.stat.last_30_days' %></th>
</tr>
<tr>
<td class='title'><%=t 'js.about.topic_count' %></td>
<td><%= @about.stats[:topic_count] %></td>
<td><%= @about.stats[:topics_7_days] %></td>
<td><%= @about.stats[:topics_30_days] %></td>
</tr>
<tr>
<td><%=t 'js.about.post_count' %></td>
<td><%= @about.stats[:post_count] %></td>
<td><%= @about.stats[:posts_7_days] %></td>
<td><%= @about.stats[:posts_30_days] %></td>
</tr>
<tr>
<td><%=t 'js.about.user_count' %></td>
<td><%= @about.stats[:user_count] %></td>
<td><%= @about.stats[:users_7_days] %></td>
<td><%= @about.stats[:users_30_days] %></td>
</tr>
<tr>
<td><%=t 'js.about.active_user_count' %></td>
<td>&mdash;</td>
<td><%= @about.stats[:active_users_7_days] %></td>
<td><%= @about.stats[:active_users_30_days] %></td>
</tr>
<tr>
<td><%=t 'js.about.like_count' %></td>
<td><%= @about.stats[:like_count] %></td>
<td><%= @about.stats[:likes_7_days] %></td>
<td><%= @about.stats[:likes_30_days] %></td>
</tr>
</table>
</section>
</section>
<br/>
<br/>

View File

@ -11,7 +11,39 @@
<%= render_google_universal_analytics_code %>
<%= yield :head %>
<style>
img { max-width: 100%; width: auto; height: auto; }
header img {
max-width: 90% !important;
padding-bottom: 10px;
}
img { max-width: 100%; width: auto; height: auto; }
#main-outlet > div {
margin-bottom: 15px;
}
footer nav a {
margin-right: 15px;
}
footer nav {
line-height: 30px;
}
body {
max-width: 98%;
padding-left: 1%;
}
.topic-list > div > a {
margin-right: 10px;
}
.topic-list > div .posts {
margin-left: 10px;
}
.topic-list > div {
margin-bottom: 15px;
}
</style>
</head>
<body>
@ -19,7 +51,7 @@
<%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %>
<%- end %>
<header>
<a href="<%= path "/" %>"><img src="<%=SiteSetting.logo_url%>" alt="<%=SiteSetting.title%>" id="site-logo"></a>
<a href="<%= path "/" %>"><img src="<%=SiteSetting.logo_url%>" alt="<%=SiteSetting.title%>" id="site-logo" style="max-width: 400px;"></a>
</header>
<div id="main-outlet" class="wrap">
<%= yield %>

View File

@ -1,4 +1,4 @@
<%- unless mobile_view? %>
<%- if include_crawler_content? %>
<% if @category %>
<h1>
@ -25,9 +25,9 @@
</a>
<%= page_links(t) %>
<% if (!@category || @category.has_children?) && t.category %>
<span>[<a href='<%= t.category.url %>'><%= t.category.name %></a>]</span>
<span class='category'>[<a href='<%= t.category.url %>'><%= t.category.name %></a>]</span>
<% end %>
<span title='<%= t 'posts' %>'>(<a href="<%=t.last_post_url%>"><%= t.posts_count %></a>)</span>
<span class='posts' title='<%= t 'posts' %>'>(<a href="<%=t.last_post_url%>"><%= t.posts_count %></a>)</span>
</div>
<% end %>
</div>

View File

@ -20,7 +20,7 @@
<%= server_plugin_outlet "topic_header" %>
<hr>
<%- unless mobile_view? %>
<%- if include_crawler_content? %>
<% @topic_view.posts.each do |post| %>
<div itemscope itemtype='http://schema.org/Article'>

View File

@ -58,7 +58,7 @@
<div>
<%- @new_by_category.first(10).each do |c| %>
<span style='white-space: nowrap'>
<a href='<%= Discourse.base_url %><%= c[0].url %>' style='color: #<%= @anchor_color %>'><%= c[0].name %></b> <span style='color: #777; margin: 0 10px 0 5px; font-size: 0.9em;'> <%= c[1] %></span>
<a href='<%= Discourse.base_url %><%= c[0].url %>' style='color: #<%= @anchor_color %>'><%= c[0].name %></b> <span style='color: #777; margin: 0 10px 0 5px; font-size: 0.9em;'> <%= c[1] %></span></a>
</span>
<%- end %>
</div>
@ -71,7 +71,7 @@
</table>
<div class='footer'>
<%=raw(t :'user_notifications.digest.unsubscribe',
<%=raw(t 'user_notifications.digest.unsubscribe',
site_link: html_site_link(@anchor_color),
unsubscribe_link: link_to(t('user_notifications.digest.click_here'), email_unsubscribe_url(host: Discourse.base_url, key: @unsubscribe_key), {:style=>'color: #' + @anchor_color })) %>
</div>

View File

@ -1,12 +0,0 @@
<div id="simple-container">
<%if flash[:error]%>
<div class='alert alert-error'>
<%=flash[:error]%>
</div>
<%else%>
<h2><%= t 'change_email.confirmed' %></h2>
<br>
<a class="btn" href="/"><%= t('change_email.please_continue', site_name: SiteSetting.title) %></a>
<%= render partial: 'auto_redirect_home' %>
<%end%>
</div>

View File

@ -0,0 +1,15 @@
<div id="simple-container">
<% if @update_result == :authorizing_new %>
<h2><%= t 'change_email.authorizing_old.title' %></h2>
<br>
<p><%= t 'change_email.authorizing_old.description' %></p>
<% elsif @update_result == :complete %>
<h2><%= t 'change_email.confirmed' %></h2>
<br>
<a class="btn" href="/"><%= t('change_email.please_continue', site_name: SiteSetting.title) %></a>
<% else %>
<div class='alert alert-error'>
<%=t 'change_email.error' %>
</div>
<% end %>
</div>

View File

@ -147,6 +147,7 @@ module Discourse
require 'discourse_redis'
require 'logster/redis_store'
require 'freedom_patches/redis'
# Use redis for our cache
config.cache_store = DiscourseRedis.new_redis_store
$redis = DiscourseRedis.new

View File

@ -95,6 +95,12 @@ redis_host = localhost
# redis server port
redis_port = 6379
# redis slave server address
redis_slave_host =
# redis slave server port
redis_slave_port = 6379
# redis database
redis_db = 0

View File

@ -42,11 +42,12 @@ if defined?(Rack::MiniProfiler)
/^\/favicon\/proxied/
]
# For our app, let's just show mini profiler always, polling is chatty so nuke that
# we DO NOT WANT mini-profiler loading on anything but real desktops and laptops
# so let's rule out all handheld, tablet, and mobile devices
Rack::MiniProfiler.config.pre_authorize_cb = lambda do |env|
path = env['PATH_INFO']
(env['HTTP_USER_AGENT'] !~ /iPad|iPhone|Nexus 7|Android/) &&
(env['HTTP_USER_AGENT'] !~ /iPad|iPhone|Android/) &&
!skip.any?{|re| re =~ path}
end

View File

@ -1,6 +1,10 @@
# Be sure to restart your server when you modify this file.
Discourse::Application.config.session_store :cookie_store, key: '_forum_session'
Discourse::Application.config.session_store(
:cookie_store,
key: '_forum_session',
path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root
)
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information

View File

@ -364,7 +364,10 @@ en:
one: "group"
other: "groups"
members: "Members"
topics: "Topics"
posts: "Posts"
mentions: "Mentions"
messages: "Messages"
alias_levels:
title: "Who can message and @mention this group?"
nobody: "Nobody"
@ -1015,7 +1018,7 @@ en:
group_mentioned: "<i title='group mentioned' class='fa fa-at'></i><p><span>{{username}}</span> {{description}}</p>"
quoted: "<i title='quoted' class='fa fa-quote-right'></i><p><span>{{username}}</span> {{description}}</p>"
replied: "<i title='replied' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='replied' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='posted' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
edited: "<i title='edited' class='fa fa-pencil'></i><p><span>{{username}}</span> {{description}}</p>"
liked: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}</span> {{description}}</p>"
liked_2: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}}</span> {{description}}</p>"
@ -1636,6 +1639,7 @@ en:
last: "Last revision"
hide: "Hide revision"
show: "Show revision"
revert: "Revert to this revision"
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
displays:
inline:
@ -2296,6 +2300,7 @@ en:
cc: "Cc"
subject: "Subject"
body: "Body"
rejection_message: "Rejection Mail"
filters:
from_placeholder: "from@example.com"
to_placeholder: "to@example.com"
@ -2959,6 +2964,33 @@ en:
famous_link:
name: Famous Link
description: Posted an external link with at least 1000 clicks
appreciated:
name: Appreciated
description: Has received at least 1 like on 20 posts
respected:
name: Respected
description: Has received at least 2 likes on 100 posts
admired:
name: Admired
description: Has received at least 5 likes on 300 posts
out_of_love:
name: Out of Love
description: Used the maximum amount of likes in a day
higher_love:
name: Higher Love
description: Used the maximum amount of likes in a day 5 times
crazy_in_love:
name: Crazy in Love
description: Used the maximum amount of likes in a day 20 times
thank_you:
name: Thank You
description: Has at least 6 liked posts and a high like ratio
gives_back:
name: Gives Back
description: Has at least 100 liked posts and a very high like ratio
empathetic:
name: Empathetic
description: Has at least 500 liked posts and a superlative like ratio
google_search: |
<h3>Search with Google</h3>

View File

@ -179,6 +179,8 @@ es:
more: "Más"
less: "Menos"
never: "nunca"
every_30_minutes: "cada 30 minutos"
every_hour: "cada hora"
daily: "cada día"
weekly: "cada semana"
every_two_weeks: "cada dos semanas"
@ -269,7 +271,7 @@ es:
one: "Este tema tiene <b>1</b> post esperando aprobación"
other: "Este tema tiene <b>{{count}}</b> posts esperando aprobación"
confirm: "Guardar Cambios"
delete_prompt: "¿Seguro que quieres eliminar a <b>%{username}</b>? Esto eliminará todos sus posts y bloqueará su email y dirección IP."
delete_prompt: "¿Seguro que quieres eliminar a <b>%{username}</b>? Se eliminarán todos sus posts y se bloqueará su email y dirección IP."
approval:
title: "El Post Necesita Aprobación"
description: "Hemos recibido tu nuevo post pero necesita ser aprobado por un moderador antes de aparecer. Por favor, ten paciencia."
@ -326,7 +328,10 @@ es:
one: "grupo"
other: "grupos"
members: "Miembros"
topics: "Temas"
posts: "Posts"
mentions: "Menciones"
messages: "Mensajes"
alias_levels:
title: "¿Quién puede emviar mensajes y @mencionar a este grupo?"
nobody: "Nadie"
@ -433,9 +438,7 @@ es:
perm_denied_btn: "Permiso denegado"
perm_denied_expl: "Has denegado los permisos para las notificaciones en tu navegador web. Configura tu navegador para permitir notificaciones. "
disable: "Desactivar notificaciones"
currently_enabled: "(activadas actualmente)"
enable: "Activar notificaciones"
currently_disabled: "(desactivadas actualmente)"
each_browser_note: "Nota: Tendrás que cambiar esta opción para cada navegador que uses."
dismiss_notifications: "Marcador todos como leídos"
dismiss_notifications_tooltip: "Marcar todas las notificaciones no leídas como leídas"
@ -470,7 +473,7 @@ es:
muted_users: "Silenciados"
muted_users_instructions: "Omite todas las notificaciones de estos usuarios."
muted_topics_link: "Mostrar temas silenciados"
automatically_unpin_topics: "Quitar destacado automáticamente cuando el usuario llega al final del tema."
automatically_unpin_topics: "Dejar de destacar temas automáticamente cuando los leo por completo."
staff_counters:
flags_given: "reportes útiles"
flagged_posts: "posts reportados"
@ -571,12 +574,26 @@ es:
title: "Distintivo de Tarjeta de Usuario"
website: "Sitio Web"
email_settings: "E-mail"
like_notification_frequency:
title: "Notificar cuando me dan Me gusta"
always: "Con cada Me gusta que reciban mis posts"
first_time_and_daily: "Al primer Me gusta que reciben mis posts y luego diariamente si reciben más"
first_time: "Al primer Me gusta que reciben mi posts"
never: "Nunca"
email_previous_replies:
title: "Incluir respuestas previas al pie de los emails"
unless_emailed: "a menos que se hayan enviado previamente"
always: "siempre"
never: "nunca"
email_digests:
title: "Cuando no visite la página, enviarme un correo con las últimas novedades."
every_30_minutes: "cada 30 minutos"
every_hour: "cada hora"
daily: "diariamente"
every_three_days: "cada tres días"
weekly: "semanalmente"
every_two_weeks: "cada dos semanas"
email_in_reply_to: "Incluir un extracto del post al que se responde en los emails"
email_direct: "Envíame un email cuando alguien me cite, responda a mis posts, mencione mi @usuario o me invite a un tema"
email_private_messages: "Notifícame por email cuando alguien me envíe un mensaje"
email_always: "Quiero recibir notificaciones por email incluso cuando esté de forma activa por el sitio"
@ -703,9 +720,11 @@ es:
read_only_mode:
enabled: "Este sitio está en modo solo-lectura. Puedes continuar navegando pero algunas acciones como responder o dar \"me gusta\" no están disponibles por ahora."
login_disabled: "Iniciar sesión está desactivado mientras el foro esté en modo solo lectura."
logout_disabled: "Cerrar sesión está desactivado mientras el sitio se encuentre en modo de sólo lectura."
too_few_topics_and_posts_notice: "¡Vamos a <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>dar por comenzada la comunidad!</a> Hay <strong>%{currentTopics} / %{requiredTopics}</strong> temas y <strong>%{currentPosts} / %{requiredPosts}</strong> mensajes. Los nuevos visitantes necesitan algo que leer y a lo que responder."
too_few_topics_notice: "¡Vamos a <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>dar por comenzada la comunidad!</a> Hay <strong>%{currentTopics} / %{requiredTopics}</strong> temas. Los nuevos visitantes necesitan algo que leer y a lo que responder."
too_few_posts_notice: "¡Vamos a <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>dar por empezada la comunidad!</a> Hay <strong>%{currentPosts} / %{requiredPosts}</strong> mensajes. Los nuevos visitantes necesitan algo que leer y a lo que responder."
logs_error_rate_exceeded_notice: "%{timestamp}: La tasa actual de <a href='%{url}' target='_blank'>%{rate} errors/%{duration}</a> ha excedido el límite establecido en las opciones del sitio de %{siteSettingLimit} errors/%{duration}."
learn_more: "saber más..."
year: 'año'
year_desc: 'temas creados en los últimos 365 días'
@ -798,6 +817,9 @@ es:
twitter:
title: "con Twitter"
message: "Autenticando con Twitter (asegúrate de desactivar cualquier bloqueador de pop ups)"
instagram:
title: "con Instagram"
message: "Autenticando con Instagram (asegúrate que los bloqueadores de pop up no están activados)"
facebook:
title: "con Facebook"
message: "Autenticando con Facebook (asegúrate de desactivar cualquier bloqueador de pop ups)"
@ -903,9 +925,13 @@ es:
group_mentioned: "<i title='group mentioned' class='fa fa-at'></i><p><span>{{username}}</span> {{description}}</p>"
quoted: "<i title='quoted' class='fa fa-quote-right'></i><p><span>{{username}}</span> {{description}}</p>"
replied: "<i title='replied' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='replied' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='posted' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
edited: "<i title='edited' class='fa fa-pencil'></i><p><span>{{username}}</span> {{description}}</p>"
liked: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}</span> {{description}}</p>"
liked_2: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}}</span> {{description}}</p>"
liked_many:
one: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} y otro</span> {{description}}</p>"
other: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} y {{count}} otros</span> {{description}}</p>"
private_message: "<i title='private message' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_private_message: "<i title='private message' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_topic: "<i title='invited to topic' class='fa fa-hand-o-right'></i><p><span>{{username}}</span> {{description}}</p>"
@ -1013,8 +1039,8 @@ es:
top: "No hay temas en el top más vistos."
search: "No hay resultados de búsqueda."
educate:
new: '<p>Tus nuevos temas aparecen aquí.</p><p>Por defecto, los temas son considerados nuevos y mostrarán un indicador: <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">nuevo</span> si son creados en los 2 últimos días.</p><p>Puedes cambiar esto en tus <a href="%{userPrefsUrl}">preferencias</a>.</p>'
unread: '<p>Tus temas sin leer aparecen aquí.</p><p>Por defecto, los temas son considerados no leídos y mostrán contadores de post sin leer <span class="badge new-posts badge-notification">1</span> si:</p><ul><li>Creaste el tema</li><li>Respondiste al tema</li><li>Leíste el tema durante más de 4 minutos</li></ul><p>O si has establecido específicamente el tema a Seguir o Vigilar en el control de notificaciones al pie de cada tema.</p><p>Puedes cambiar esto en tus <a href="%{userPrefsUrl}">preferencias</a>.</p>'
new: '<p>Tus temas nuevos aparecen aquí.</p><p>Por defecto, los temas se consideran nuevos y mostrarán un indicador <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">nuevo</span> si fueron creados en los últimos 2 días.</p><p>Dirígite a <a href="%{userPrefsUrl}">preferencias</a> para cambiar esto.</p>'
unread: '<p>Tus temas sin leer aparecen aquí.</p><p>Por defecto, los temas son considerados sin leer y mostrarán contadores de posts sin leer <span class="badge new-posts badge-notification">1</span> si:</p><ul><li>Creaste el tema</li><li>Respondiste al tema</li><li>Leíste el tema por más de 4 minutos</li></ul><p>O si has establecido específicamente el tema como Siguiendo o Vigilando a través del control de notificaciones al pie de cada tema.</p><p>Visita tus <a href="%{userPrefsUrl}">preferencias</a> para cambiar esto.</p>'
bottom:
latest: "No hay más temas recientes para leer."
hot: "No hay más temas candentes."
@ -1379,17 +1405,14 @@ es:
like: "Deshacer Me gusta"
vote: "Deshacer voto"
people:
off_topic: "{{icons}} reportó esto como off-topic"
spam: "{{icons}} reportó esto como spam"
spam_with_url: "{{icons}} reportó <a href='{{postUrl}}'>esto como spam</a>"
inappropriate: "{{icons}} flagged reportó esto como inapropiado"
notify_moderators: "{{icons}} ha notificado a los moderadores"
notify_moderators_with_url: "{{icons}} <a href='{{postUrl}}'>moderadores notificados</a>"
notify_user: "{{icons}} ha enviado un mensaje"
notify_user_with_url: "{{icons}} ha enviado un <a href='{{postUrl}}'>mensaje</a>"
bookmark: "{{icons}} ha marcado esto"
like: "{{icons}} les gusta esto"
vote: "{{icons}} ha votado esto"
off_topic: "reportó esto como off-topic"
spam: "reportó esto como spam"
inappropriate: "reportó esto como inapropiado"
notify_moderators: "notificó a moderadores"
notify_user: "envió un mensaje"
bookmark: "guardó esto en marcadores"
like: "le gustó esto"
vote: "votó por esto"
by_you:
off_topic: "Has reportado esto como off-topic"
spam: "Has reportado esto como Spam"
@ -1461,6 +1484,7 @@ es:
last: "Última revisión"
hide: "Ocultar revisión."
show: "Mostrar revisión."
revert: "Volver a esta revisión"
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
displays:
inline:
@ -1541,7 +1565,6 @@ es:
description: "No serás notificado de ningún tema en estas categorías, y no aparecerán en la página de mensajes recientes."
flagging:
title: '¡Gracias por ayudar a mantener una comunidad civilizada!'
private_reminder: 'los reportes son privados, son visibles <b>únicamente</b> por los administradores'
action: 'Reportar post'
take_action: "Tomar medidas"
notify_action: 'Mensaje'
@ -1553,7 +1576,7 @@ es:
submit_tooltip: "Enviar el reporte privado"
take_action_tooltip: "Alcanzar el umbral de reportes inmediatamente, en vez de esperar a más reportes de la comunidad"
cant: "Lo sentimos, no puedes reportar este post en este momento."
notify_staff: 'Notificar al Staff'
notify_staff: 'Notificar a los administradores de forma privada'
formatted_name:
off_topic: "Está fuera de lugar"
inappropriate: "Es inapropiado"
@ -1934,11 +1957,11 @@ es:
is_disabled: "Restaurar está deshabilitado en la configuración del sitio."
label: "Restaurar"
title: "Restaurar la copia de seguridad"
confirm: "¿Estás seguro que quieres restaurar esta copia de seguridad?"
confirm: "¿Seguro que quieres restaurar esta copia de seguridad?"
rollback:
label: "Revertir"
title: "Regresar la base de datos al estado funcional anterior"
confirm: "¿Estás seguro que quieres regresar la base de datos al estado funcional anterior?"
confirm: "¿Seguro que quieres retornar la base de datos al estado funcional previo?"
export_csv:
user_archive_confirm: "¿Seguro que quieres descargar todos tus posts?"
success: "Exportación iniciada, se te notificará a través de un mensaje cuando el proceso se haya completado."
@ -2041,9 +2064,6 @@ es:
love:
name: 'me gusta'
description: "El color del botón de \"me gusta\""
wiki:
name: 'wiki'
description: "Color base usado para el fondo en los posts del wiki."
email:
title: "Emails"
settings: "Ajustes"
@ -2080,6 +2100,20 @@ es:
subject: "Asunto"
error: "Error"
none: "No se encontraron emails entrantes."
modal:
title: "Detalles de emails entrantes"
error: "Error"
return_path: "Ruta de retorno"
message_id: "Id del mensaje"
in_reply_to: "En respuesta a"
references: "Referencias"
date: "Fecha"
from: "De"
to: "Para"
cc: "Cc"
subject: "Asunto"
body: "Cuerpo"
rejection_message: "Correo de rechazo"
filters:
from_placeholder: "from@example.com"
to_placeholder: "to@example.com"
@ -2155,6 +2189,7 @@ es:
revoke_admin: "revocar administración"
grant_moderation: "conceder moderación"
revoke_moderation: "revocar moderación"
backup_operation: "operación de copia de seguridad de respaldo"
screened_emails:
title: "Correos bloqueados"
description: "Cuando alguien trata de crear una cuenta nueva, los siguientes correos serán revisados y el registro será bloqueado, o alguna otra acción será realizada."

View File

@ -179,6 +179,8 @@ fi:
more: "Lisää"
less: "Vähemmän"
never: "ei koskaan"
every_30_minutes: "puolen tunnin välein"
every_hour: "tunnin välein"
daily: "päivittäin"
weekly: "viikottain"
every_two_weeks: "kahden viikon välein"
@ -326,7 +328,10 @@ fi:
one: "ryhmä"
other: "ryhmät"
members: "Jäsenet"
topics: "Ketjut"
posts: "Viestit"
mentions: "Viittaukset"
messages: "Viestit"
alias_levels:
title: "Ketkä voivat lähettää viestejä tälle ryhmälle tai @viitata siihen?"
nobody: "Ei kukaan"
@ -468,7 +473,7 @@ fi:
muted_users: "Vaimennetut"
muted_users_instructions: "Älä näytä ilmoituksia näiltä käyttäjiltä"
muted_topics_link: "Näytä vaimennetut ketjut"
automatically_unpin_topics: "Poista ketjun kiinnitys automaattisesti, jos selaan keskustelun loppuun"
automatically_unpin_topics: "Poista kiinnitetyn ketjun kiinnitys automaattisesti, selattuani sen loppuun."
staff_counters:
flags_given: "hyödyllistä liputusta"
flagged_posts: "liputettuja viestejä"
@ -569,6 +574,12 @@ fi:
title: "Käyttäjäkortin tunnus"
website: "Nettisivu"
email_settings: "Sähköposti"
like_notification_frequency:
title: "Ilmoita, kun viestistäni tykätään"
always: "Aina"
first_time_and_daily: "Ensimmäistä kertaa ja päivittäin"
first_time: "Ensimmäistä kertaa"
never: "Ei koskaan"
email_previous_replies:
title: "Liitä aiemmat vastaukset mukaan sähköpostin alaosaan"
unless_emailed: "ellei aiemmin lähetetty"
@ -576,6 +587,8 @@ fi:
never: "ei koskaan"
email_digests:
title: "Lähetä tiivistelmä uusista viesteistä sähköpostilla, jos en käy sivustolla "
every_30_minutes: "puolen tunnin välein"
every_hour: "tunneittain"
daily: "päivittäin"
every_three_days: "joka kolmas päivä"
weekly: "viikottain"
@ -651,7 +664,7 @@ fi:
summary:
title: "Yhteenveto"
stats: "Tilastot"
topic_count: "Avattuja ketjuja"
topic_count: "Luotuja ketjuja"
post_count: "Kirjoitettuja viestejä"
likes_given: "Annettuja tykkäyksiä"
likes_received: "Saatuja tykkäyksiä"
@ -706,10 +719,12 @@ fi:
refresh: "Lataa sivu uudelleen"
read_only_mode:
enabled: "Sivusto on vain luku -tilassa. Voit jatkaa selailua, mutta vastaaminen, tykkääminen ja muita toimintoja on toistaiseksi poissa käytöstä."
login_disabled: "Kirjautuminen ei ole käytössä sivuston ollessa vain luku -tilassa."
login_disabled: "Et voi kirjautua sisään, kun sivusto on vain luku -tilassa."
logout_disabled: "Et voi kirjautua ulos, kun sivusto on vain luku -tilassa."
too_few_topics_and_posts_notice: "Laitetaanpa <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>keskustelu alulle!</a> Tällä hetkellä palstalla on <strong>%{currentTopics} / %{requiredTopics}</strong> ketjua ja <strong>%{currentPosts} / %{requiredPosts}</strong> viestiä. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata."
too_few_topics_notice: "Laitetaanpa <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>keskustelu alulle!</a> Tällä hetkellä palstalla on <strong>%{currentTopics} / %{requiredTopics}</strong> ketjua. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata."
too_few_posts_notice: "Laitetaanpa <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>keskustelu alulle!</a> Tällä hetkellä palstalla on <strong>%{currentPosts} / %{requiredPosts}</strong> viestiä. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata."
logs_error_rate_exceeded_notice: "%{timestamp}: Current rate of <a href='%{url}' target='_blank'>%{rate} errors/%{duration}</a> has exceeded site settings's limit of %{siteSettingLimit} errors/%{duration}."
learn_more: "opi lisää..."
year: 'vuosi'
year_desc: 'viimeisen 365 päivän aikana luodut ketjut'
@ -802,6 +817,9 @@ fi:
twitter:
title: "Twitterillä"
message: "Todennetaan Twitterin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)"
instagram:
title: "Instagramilla"
message: "Todennetaan Instagramin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)"
facebook:
title: "Facebookilla"
message: "Todennetaan Facebookin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)"
@ -907,9 +925,13 @@ fi:
group_mentioned: "<i title='group mentioned' class='fa fa-at'></i><p><span>{{username}}</span> {{description}}</p>"
quoted: "<i title='lainasi' class='fa fa-quote-right'></i><p><span>{{username}}</span> {{description}}</p>"
replied: "<i title='vastasi' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='vastasi' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='kirjoitti' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
edited: "<i title='muokkasi' class='fa fa-pencil'></i><p><span>{{username}}</span> {{description}}</p>"
liked: "<i title='tykkäsi' class='fa fa-heart'></i><p><span>{{username}}</span> {{description}}</p>"
liked_2: "<i title='tykkäsi' class='fa fa-heart'></i><p><span>{{username}}, {{username2}}</span> {{description}}</p>"
liked_many:
one: "<i title='tykkäsi' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} ja 1 muu</span> {{description}}</p>"
other: "<i title='tykkäsi' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} ja {{count}} muuta</span> {{description}}</p>"
private_message: "<i title='yksityisviesti' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_private_message: "<i title='yksityisviesti' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_topic: "<i title='kutsui ketjuun' class='fa fa-hand-o-right'></i><p><span>{{username}}</span> {{description}}</p>"
@ -1016,6 +1038,9 @@ fi:
category: "Alueella {{category}} ei ole ketjua."
top: "Huippuketjuja ei ole."
search: "Hakutuloksia ei löytynyt."
educate:
new: '<p>Uuden ketjut näytetään tässä.</p><p>Ketjut tulkitaan uusiksi ja niiden yhteydessä näytetään <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">uusi</span>-merkki, kun ne on luotu edellisen kahden päivän aikana.</p><p>Voit muuttaa tätä <a href="%{userPrefsUrl}">käyttäjäasetuksistasi</a>.</p>'
unread: '<p>Lukemattomia viestejä sisältävät ketjut näytetään tässä.</p><p>Ketjun yhteydessä näytetään lukemattomien viestien lukumäärä <span class="badge new-posts badge-notification">1</span> jos olet:</p><ul><li>luonut ketjun</li><li>vastannut ketjuun</li><li>lukenut ketjua pidempään, kuin 4 minuuttia</li></ul><p>tai, jos olet erikseen merkannut ketjun seurattavaksi tai tarkkailtavaksi ketjun lopusta löytyvästä painikkeesta.</p><p>Voit muuttaa tätä <a href="%{userPrefsUrl}">käyttäjäasetuksistasi</a>.</p>'
bottom:
latest: "Tuoreimpia ketjuja ei ole enempää."
hot: "Kuumia ketjuja ei ole enempää."
@ -1379,6 +1404,15 @@ fi:
bookmark: "Peru kirjanmerkki"
like: "Peru tykkäys"
vote: "Peru ääni"
people:
off_topic: "liputti tämän asiaan kuulumattomaksi"
spam: "liputti tämän roskapostiksi"
inappropriate: "liputti tämän asiattomaksi"
notify_moderators: "ilmoitti valvojille"
notify_user: "lähetti viestin"
bookmark: "lisäsi tämän kirjanmerkkeihin"
like: "tykkäsi tästä"
vote: "äänesti tätä"
by_you:
off_topic: "Liputit tämän asiaankuulumattomaksi"
spam: "Liputit tämän roskapostiksi"
@ -1450,6 +1484,7 @@ fi:
last: "Viimeinen revisio"
hide: "Piilota revisio"
show: "Näytä revisio"
revert: "Palaa tähän revisioon"
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
displays:
inline:
@ -1530,7 +1565,6 @@ fi:
description: "Et saa ilmoituksia uusista ketjuista näillä alueilla, eivätkä ne näy tuoreimmissa."
flagging:
title: 'Kiitos avustasi yhteisön hyväksi!'
private_reminder: 'liput ovat yksityisiä, ne näkyvät <b>ainoastaan</b> henkilökunnalle'
action: 'Liputa viesti'
take_action: "Ryhdy toimiin"
notify_action: 'Viesti'
@ -1542,7 +1576,7 @@ fi:
submit_tooltip: "Toimita lippu"
take_action_tooltip: "Saavuta liputusraja välittömästi, ennemmin kuin odota muidenkin käyttäjien liputuksia."
cant: "Pahoittelut, et pysty liputtamaan tätä viestiä tällä hetkellä."
notify_staff: 'Ilmoita ylläpitäjille'
notify_staff: 'Ilmoita ylläpidolle yksityisesti'
formatted_name:
off_topic: "Se on asiaankuulumaton"
inappropriate: "Se on asiaton"
@ -1923,9 +1957,11 @@ fi:
is_disabled: "Palautus on estetty sivuston asetuksissa."
label: "Palauta"
title: "Palauta varmuuskopio"
confirm: "Oletko varma, että haluat palauttaa tämän varmuuskopion?"
rollback:
label: "Palauta"
title: "Palauta tietokanta edelliseen toimivaan tilaan"
confirm: "Oletko varma, että haluat palauttaa tietokannan edelliseen toimivaan tilaan?"
export_csv:
user_archive_confirm: "Oletko varma, että haluat ladata viestisi?"
success: "Vienti on käynnissä. Saat ilmoituksen viestillä, kun prosessi on valmis."
@ -2065,12 +2101,19 @@ fi:
error: "Virhe"
none: "Uusia sähköpostiviestejä ei löydetty."
modal:
title: "Saapuvan sähköpostin tiedot"
error: "Virhe"
return_path: "Paluupolku"
message_id: "Viestin ID"
in_reply_to: "Vastauksena"
references: "Viittaukset"
date: "Päivämäärä"
from: "Lähettäjä"
to: "Vastaanottaja"
cc: "Kopio"
subject: "Otsikko"
body: "Leipäteksti"
rejection_message: "Hylkäysviesti"
filters:
from_placeholder: "from@example.com"
to_placeholder: "to@example.com"
@ -2248,6 +2291,7 @@ fi:
moderator: "Valvoja?"
admin: "Ylläpitäjä?"
blocked: "Estetty?"
staged: "Luotu?"
show_admin_profile: "Ylläpito"
edit_title: "Muokkaa nimikettä"
save_title: "Tallenna nimike"
@ -2317,6 +2361,7 @@ fi:
deactivate_explanation: "Käytöstä poistetun käyttäjän täytyy uudelleen vahvistaa sähköpostiosoitteensa."
suspended_explanation: "Hyllytetty käyttäjä ei voi kirjautua sisään."
block_explanation: "Estetty käyttäjä ei voi luoda viestejä tai ketjuja."
stage_explanation: "Automaattisesti luotu käyttäjä voi vastata sähköpostilla vain erikseen määritettyihin ketjuihin."
trust_level_change_failed: "Käyttäjän luottamustason vaihtamisessa tapahtui virhe."
suspend_modal_title: "Hyllytä käyttäjä"
trust_level_2_users: "Käyttäjät luottamustasolla 2"

View File

@ -179,6 +179,8 @@ fr:
more: "Plus"
less: "Moins"
never: "jamais"
every_30_minutes: "toutes les 30 minutes"
every_hour: "chaque heure"
daily: "quotidiennes"
weekly: "hebdomadaires"
every_two_weeks: "bi-mensuelles"
@ -326,7 +328,10 @@ fr:
one: "groupe"
other: "groupes"
members: "Membres"
topics: "Sujets"
posts: "Messages"
mentions: "Mentions"
messages: "Messages"
alias_levels:
title: "Qui peut envoyer un message et @notifier ce groupe ?"
nobody: "Personne"
@ -468,7 +473,7 @@ fr:
muted_users: "Silencieux"
muted_users_instructions: "Cacher toutes les notifications de ces utilisateurs."
muted_topics_link: "Afficher les sujets en sourdine"
automatically_unpin_topics: "Automatiquement désépingler les sujets lorsque vous atteignez le bas."
automatically_unpin_topics: "Desépingler automatiquement quand j'arrive à la fin."
staff_counters:
flags_given: "signalements utiles"
flagged_posts: "messages signalés"
@ -569,6 +574,12 @@ fr:
title: "Badge pour la carte de l'utilisateur"
website: "Site internet"
email_settings: "Courriel"
like_notification_frequency:
title: "Notifier lors d'un J'aime"
always: "Toujours"
first_time_and_daily: "La première fois qu'un message est aimé, et quotidiennement"
first_time: "La première fois qu'un message est aimé"
never: "Jamais"
email_previous_replies:
title: "Inclure les réponses précédentes en bas des courriels"
unless_emailed: "sauf si déjà envoyé"
@ -576,6 +587,8 @@ fr:
never: "jamais"
email_digests:
title: "Quand je ne visite pas ce site, m'envoyer un résumé des nouveautés par courriel:"
every_30_minutes: "toutes les 30 minutes"
every_hour: "toutes les heures"
daily: "quotidien"
every_three_days: "tous les trois jours"
weekly: "hebdomadaire"
@ -707,9 +720,11 @@ fr:
read_only_mode:
enabled: "Le site est en mode lecture seule. Vous pouvez continer à naviguer, mais les réponses, J'aime et autre interactions sont désactivées pour l'instant."
login_disabled: "Impossible de se connecté quand le site est en mode lecture seule."
logout_disabled: "Impossible de se deconnecter quand le site est en mode lecture seule."
too_few_topics_and_posts_notice: "<a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>Démarrons cette discussion!</a> Il y a actuellement <strong>%{currentTopics} / %{requiredTopics}</strong> sujets et <strong>%{currentPosts} / %{requiredPosts}</strong> messages. Les nouveaux visiteurs ont besoin de quelques conversations pour lire et répondre."
too_few_topics_notice: "<a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>Démarrons cette discussion !</a> Il y a actuellement <strong>%{currentTopics} / %{requiredTopics}</strong> sujets. Les nouveaux visiteurs ont besoin de quelques conversations à lire et répondre."
too_few_posts_notice: "<a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>Démarrons cette discussion !</a> Il y a actuellement <strong>%{currentPosts} / %{requiredPosts}</strong> messages. Les nouveaux visiteurs ont besoin de quelques conversations à lire et répondre."
logs_error_rate_exceeded_notice: "%{timestamp}: Le taux actuel de <a href='%{url}' target='_blank'>%{rate} erreurs/%{duration}</a> a dépassé la limite des paramètres du site de %{siteSettingLimit} erreurs/%{duration}."
learn_more: "en savoir plus…"
year: 'an'
year_desc: 'sujets créés durant les 365 derniers jours'
@ -802,6 +817,9 @@ fr:
twitter:
title: "via Twitter"
message: "Authentification via Twitter (assurez-vous que les popups ne soient pas bloquées)"
instagram:
title: "avec Instagram"
message: "Authentification via Instagtram (assurez-vous que les popups ne soient pas bloquées)"
facebook:
title: "via Facebook"
message: "Authentification via Facebook (assurez-vous que les popups ne soient pas bloquées)"
@ -907,9 +925,13 @@ fr:
group_mentioned: "<i title='group mentioned' class='fa fa-at'></i><p><span>{{username}}</span> {{description}}</p>"
quoted: "<i title='cité' class='fa fa-quote-right'></i><p><span>{{username}}</span> {{description}}</p>"
replied: "<i title='avec réponse' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='avec réponse' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
posted: "<i title='posted' class='fa fa-reply'></i><p><span>{{username}}</span> {{description}}</p>"
edited: "<i title='édité' class='fa fa-pencil'></i><p><span>{{username}}</span> {{description}}</p>"
liked: "<i title='aimé' class='fa fa-heart'></i><p><span>{{username}}</span> {{description}}</p>"
liked_2: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}}</span> {{description}}</p>"
liked_many:
one: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} et 1 autre</span> {{description}}</p>"
other: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}, {{username2}} et {{count}} autres</span> {{description}}</p>"
private_message: "<i title='message privé' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_private_message: "<i title='message privé' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
invited_to_topic: "<i title='invité' class='fa fa-hand-o-right'></i><p><span>{{username}}</span> {{description}}</p>"
@ -1462,6 +1484,7 @@ fr:
last: "Dernière révision"
hide: "Masquer la révision"
show: "Afficher la révision"
revert: "Revenir à cette révision"
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
displays:
inline:
@ -1542,7 +1565,6 @@ fr:
description: "Vous ne serez jamais notifié de rien concernant les nouveaux sujets dans ces catégories, et elles n'apparaîtront pas dans les dernières catégories."
flagging:
title: 'Merci de nous aider à garder notre communauté aimable !'
private_reminder: 'les signalements sont privés, <b>seulement</b> visible aux modérateurs'
action: 'Signaler ce message'
take_action: "Signaler"
notify_action: 'Message'
@ -1554,7 +1576,7 @@ fr:
submit_tooltip: "Soumettre le signalement privé"
take_action_tooltip: "Atteindre le seuil de signalement immédiatement, plutôt que d'attendre plus de signalement de la communauté."
cant: "Désolé, vous ne pouvez pas signaler ce message pour le moment"
notify_staff: 'Notifier les responsables'
notify_staff: 'Notifier les responsables de manière privée'
formatted_name:
off_topic: "C'est hors-sujet"
inappropriate: "C'est inapproprié"
@ -2087,10 +2109,11 @@ fr:
references: "References"
date: "Date"
from: "From"
to: "à"
to: "To"
cc: "Cc"
subject: "Objet"
body: "Corps"
subject: "Subject"
body: "Body"
rejection_message: "Courriel de refus"
filters:
from_placeholder: "from@example.com"
to_placeholder: "to@example.com"

View File

@ -713,7 +713,7 @@ it:
summary:
enabled_description: "Stai visualizzando un riepilogo dell'argomento: è la comunità a determinare quali sono i messaggi più interessanti."
description: "Ci sono <b>{{replyCount}}</b> risposte."
description_time: "Ci sono <b>{{replyCount}}</b> risposte con un tempo stimato di lettura di <b>{{readingTime}} minuti</b>."
description_time: "Ci sono <b>{{replyCount}}</b> risposte con un tempo stimato di lettura di <b>{{readingTime}} minuti</b>."
enable: 'Riassumi Questo Argomento'
disable: 'Mostra Tutti i Messaggi'
deleted_filter:

View File

@ -159,6 +159,8 @@ ko:
more: "더"
less: "덜"
never: "전혀"
every_30_minutes: "매 30분 마다"
every_hour: "매 한시간 마다"
daily: "매일"
weekly: "매주"
every_two_weeks: "격주"
@ -545,6 +547,7 @@ ko:
never: "절대"
email_digests:
title: "사이트 방문이 없을 경우, 새로운 글을 요약하여 메일로 보냄"
every_30_minutes: "매 30분 마다"
daily: "매일"
every_three_days: "매 3일마다"
weekly: "매주"

View File

@ -179,6 +179,8 @@ nl:
more: "Meer"
less: "Minder"
never: "nooit"
every_30_minutes: "elke dertig minuten"
every_hour: "elk uur"
daily: "dagelijks"
weekly: "wekelijks"
every_two_weeks: "elke twee weken"
@ -468,7 +470,7 @@ nl:
muted_users: "Negeren"
muted_users_instructions: "Negeer alle meldingen van deze leden."
muted_topics_link: "Toon gedempte topics."
automatically_unpin_topics: "ontspelt onderwerp automatische wanneer de bodem is bereikt."
automatically_unpin_topics: "Topics automatisch lospinnen als ik het laatste bericht bereik."
staff_counters:
flags_given: "behulpzame markeringen"
flagged_posts: "gemarkeerde berichten"
@ -569,6 +571,11 @@ nl:
title: "Badge van gebruikersprofiel"
website: "Website"
email_settings: "E-mail"
like_notification_frequency:
always: "Altijd"
first_time_and_daily: "De eerste keer dat iemand een bericht leuk vond en dagelijks"
first_time: "De eerste keer dat iemand een bericht leuk vond"
never: "Nooit"
email_previous_replies:
title: "Voeg de vorige reacties bij onderaan de emails"
unless_emailed: "tenzij eerder verzonden"
@ -576,6 +583,8 @@ nl:
never: "nooit"
email_digests:
title: "Stuur me een mail met de laatste updates wanneer ik de site niet bezoek:"
every_30_minutes: "elke dertig minuten"
every_hour: "elk uur"
daily: "dagelijks"
every_three_days: "elke drie dagen"
weekly: "wekelijks"
@ -707,6 +716,7 @@ nl:
read_only_mode:
enabled: "De site is in alleen lezen modus. Interactie is niet mogelijk."
login_disabled: "Zolang de site in read-only modus is, kan er niet ingelogd worden."
logout_disabled: "Uitloggen is uitgeschakeld als de site op alleen lezen staat."
too_few_topics_and_posts_notice: "Laten <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>we de discussie starten!</a> Er zijn al <strong>%{currentTopics} / %{requiredTopics}</strong> topics en <strong>%{currentPosts} / %{requiredPosts}</strong> berichten. Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren."
too_few_topics_notice: "Laten <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>we de discussie starten!</a> Er zijn al <strong>%{currentTopics} / %{requiredTopics}</strong> topics en <strong>%{currentPosts} / %{requiredPosts}</strong> berichten. Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren."
too_few_posts_notice: "Laten <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>we de discussie starten!</a>. Er zijn al <strong>%{currentPosts} / %{requiredPosts}</strong> posts Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren."
@ -802,6 +812,9 @@ nl:
twitter:
title: "met Twitter"
message: "Inloggen met een Twitteraccount (zorg ervoor dat je popup blocker uit staat)"
instagram:
title: "met Instagram"
message: "Inloggen met een Instagram-account (zorg ervoor dat je pop-upblocker uitstaat)."
facebook:
title: "met Facebook"
message: "Inloggen met een Facebookaccount (zorg ervoor dat je popup blocker uit staat)"
@ -1542,7 +1555,6 @@ nl:
description: "Je zult nooit op de hoogte worden gebracht over nieuwe topics in deze categorie, en ze zullen niet verschijnen in Nieuwste."
flagging:
title: 'Bedankt voor het helpen beleefd houden van onze gemeenschap!'
private_reminder: 'vlaggen zijn privé, <b>alleen</b> zichtbaar voor de staf'
action: 'Meld bericht'
take_action: "Onderneem actie"
notify_action: 'Bericht'
@ -1554,7 +1566,6 @@ nl:
submit_tooltip: "Verstuur de privé markering"
take_action_tooltip: "Bereik de vlag drempel direct, in plaats van het wachten op meer gemeenschapsvlaggen"
cant: "Sorry, je kan dit bericht momenteel niet melden."
notify_staff: 'Licht de staf in'
formatted_name:
off_topic: "Het is off topic"
inappropriate: "Het is ongepast"

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