Version bump
This commit is contained in:
commit
919f33f377
2
Gemfile
2
Gemfile
@ -120,6 +120,8 @@ group :test do
|
||||
gem 'fakeweb', '~> 1.3.0', require: false
|
||||
gem 'minitest', require: false
|
||||
gem 'timecop'
|
||||
# TODO: Remove once we upgrade to Rails 5.
|
||||
gem 'test_after_commit'
|
||||
end
|
||||
|
||||
group :test, :development do
|
||||
|
||||
13
Gemfile.lock
13
Gemfile.lock
@ -138,7 +138,7 @@ GEM
|
||||
json (1.8.6)
|
||||
jwt (1.5.2)
|
||||
kgio (2.10.0)
|
||||
libv8 (5.3.332.38.3)
|
||||
libv8 (5.3.332.38.5)
|
||||
listen (0.7.3)
|
||||
logster (1.2.7)
|
||||
loofah (2.0.3)
|
||||
@ -153,7 +153,7 @@ GEM
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.2)
|
||||
mini_portile2 (2.1.0)
|
||||
mini_racer (0.1.7)
|
||||
mini_racer (0.1.9)
|
||||
libv8 (~> 5.3)
|
||||
minitest (5.9.1)
|
||||
mocha (1.1.0)
|
||||
@ -206,7 +206,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.2)
|
||||
onebox (1.8.3)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3.4)
|
||||
moneta (~> 0.8)
|
||||
@ -276,7 +276,7 @@ GEM
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
trollop (>= 1.16.2)
|
||||
redis (3.3.1)
|
||||
redis (3.3.3)
|
||||
redis-namespace (1.5.2)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
rest-client (1.8.0)
|
||||
@ -359,6 +359,8 @@ GEM
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
stackprof (0.2.10)
|
||||
test_after_commit (1.1.0)
|
||||
activerecord (>= 3.2)
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.5)
|
||||
@ -470,6 +472,7 @@ DEPENDENCIES
|
||||
sinatra
|
||||
spork-rails
|
||||
stackprof
|
||||
test_after_commit
|
||||
thor
|
||||
timecop
|
||||
uglifier
|
||||
@ -477,4 +480,4 @@ DEPENDENCIES
|
||||
unicorn
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.3
|
||||
1.14.6
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { iconHTML } from 'discourse-common/helpers/fa-icon';
|
||||
import { bufferedRender } from 'discourse-common/lib/buffered-render';
|
||||
|
||||
export default Ember.Component.extend(bufferedRender({
|
||||
tagName: 'th',
|
||||
classNames: ['sortable'],
|
||||
rerenderTriggers: ['order', 'ascending'],
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const icon = this.get('icon');
|
||||
|
||||
if (icon) {
|
||||
buffer.push(iconHTML(icon));
|
||||
}
|
||||
|
||||
buffer.push(I18n.t(this.get('i18nKey')));
|
||||
|
||||
if (this.get('field') === this.get('order')) {
|
||||
buffer.push(iconHTML(this.get('ascending') ? 'chevron-up' : 'chevron-down'));
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
const currentOrder = this.get('order');
|
||||
const field = this.get('field');
|
||||
|
||||
if (currentOrder === field) {
|
||||
this.set('ascending', this.get('ascending') ? null : true);
|
||||
} else {
|
||||
this.setProperties({ order: field, ascending: null });
|
||||
}
|
||||
}
|
||||
}));
|
||||
@ -0,0 +1,11 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsType: Ember.inject.controller(),
|
||||
sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"),
|
||||
|
||||
@computed("sortedGroups")
|
||||
messageKey(sortedGroups) {
|
||||
return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`;
|
||||
}
|
||||
});
|
||||
@ -1,9 +1,14 @@
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import { i18n } from 'discourse/lib/computed';
|
||||
import AdminUser from 'admin/models/admin-user';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
query: null,
|
||||
queryParams: ['order', 'ascending'],
|
||||
order: 'seen',
|
||||
ascending: null,
|
||||
showEmails: false,
|
||||
refreshing: false,
|
||||
listFilter: null,
|
||||
@ -39,14 +44,15 @@ export default Ember.Controller.extend({
|
||||
this._refreshUsers();
|
||||
}, 250).observes('listFilter'),
|
||||
|
||||
|
||||
@observes('order', 'ascending')
|
||||
_refreshUsers: function() {
|
||||
var self = this;
|
||||
this.set('refreshing', true);
|
||||
|
||||
AdminUser.findAll(this.get('query'), { filter: this.get('listFilter'), show_emails: this.get('showEmails') }).then(function (result) {
|
||||
self.set('model', result);
|
||||
}).finally(function() {
|
||||
self.set('refreshing', false);
|
||||
AdminUser.findAll(this.get('query'), { filter: this.get('listFilter'), show_emails: this.get('showEmails'), order: this.get('order'), ascending: this.get('ascending') }).then( (result) => {
|
||||
this.set('model', result);
|
||||
}).finally( () => {
|
||||
this.set('refreshing', false);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -85,7 +85,7 @@ export default Discourse.Route.extend({
|
||||
if (confirmed) {
|
||||
Discourse.User.currentProp("hideReadOnlyAlert", true);
|
||||
backup.restore().then(function() {
|
||||
self.controllerFor("adminBackupsLogs").clear();
|
||||
self.controllerFor("adminBackupsLogs").get("logs").clear();
|
||||
self.controllerFor("adminBackups").set("model.isOperationRunning", true);
|
||||
self.transitionTo("admin.backups.logs");
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class="current-style {{if maximized 'maximized'}}">
|
||||
<div class='wrapper'>
|
||||
{{text-field class="style-name" value=model.name}}
|
||||
<a class="btn export" download target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<ul class="nav nav-pills">
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
<div class="groups-type-index">
|
||||
<p>{{i18n messageKey}}</p>
|
||||
|
||||
<div>
|
||||
{{#link-to 'adminGroup' 'new' class="btn"}}
|
||||
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,29 +1,31 @@
|
||||
<div class='row groups'>
|
||||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.groups.edit'}}</h3>
|
||||
<ul>
|
||||
{{#each sortedGroups as |group|}}
|
||||
<li>
|
||||
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
|
||||
{{#if group.userCountDisplay}}
|
||||
<span class="count">{{number group.userCountDisplay}}</span>
|
||||
{{/if}}
|
||||
{{#if sortedGroups}}
|
||||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.groups.edit'}}</h3>
|
||||
<ul>
|
||||
{{#each sortedGroups as |group|}}
|
||||
<li>
|
||||
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
|
||||
{{#if group.userCountDisplay}}
|
||||
<span class="count">{{number group.userCountDisplay}}</span>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<div class='controls'>
|
||||
{{#if isAuto}}
|
||||
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
|
||||
{{else}}
|
||||
{{#link-to 'adminGroup' 'new' class="btn"}}
|
||||
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<div class='controls'>
|
||||
{{#if isAuto}}
|
||||
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
|
||||
{{else}}
|
||||
{{#link-to 'adminGroup' 'new' class="btn"}}
|
||||
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='content-editor'>
|
||||
<div class="span13">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
{{#if model}}
|
||||
<table class='table users-list'>
|
||||
<tr>
|
||||
<thead>
|
||||
{{#if showApproval}}
|
||||
<th>{{input type="checkbox" checked=selectAll}}</th>
|
||||
{{/if}}
|
||||
@ -30,54 +30,79 @@
|
||||
<th>{{i18n 'username'}}</th>
|
||||
<th>{{i18n 'email'}}</th>
|
||||
<th>{{i18n 'admin.users.last_emailed'}}</th>
|
||||
<th>{{i18n 'last_seen'}}</th>
|
||||
<th>{{i18n 'admin.user.topics_entered'}}</th>
|
||||
<th>{{i18n 'admin.user.posts_read_count'}}</th>
|
||||
<th>{{i18n 'admin.user.time_read'}}</th>
|
||||
<th>{{i18n 'created'}}</th>
|
||||
{{admin-directory-toggle field="seen" i18nKey='last_seen' order=order ascending=ascending}}
|
||||
{{admin-directory-toggle field="topics_viewed" i18nKey="admin.user.topics_entered" order=order ascending=ascending}}
|
||||
{{admin-directory-toggle field="posts_read" i18nKey="admin.user.posts_read_count" order=order ascending=ascending}}
|
||||
{{admin-directory-toggle field="read_time" i18nKey="admin.user.time_read" order=order ascending=ascending}}
|
||||
{{admin-directory-toggle field="created" i18nKey="created" order=order ascending=ascending}}
|
||||
{{#if showApproval}}
|
||||
<th>{{i18n 'admin.users.approved'}}</th>
|
||||
{{/if}}
|
||||
<th> </th>
|
||||
</tr>
|
||||
|
||||
{{#each model as |user|}}
|
||||
<tr class="user {{user.selected}} {{unless user.active 'not-activated'}}">
|
||||
{{#if showApproval}}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each model as |user|}}
|
||||
<tr class="user {{user.selected}} {{unless user.active 'not-activated'}}">
|
||||
{{#if showApproval}}
|
||||
<td>
|
||||
{{#if user.can_approve}}
|
||||
{{input type="checkbox" checked=user.selected}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/if}}
|
||||
<td><a href="{{unbound user.path}}" data-user-card="{{unbound user.username}}">{{avatar user imageSize="small"}}</a></td>
|
||||
<td>{{#link-to 'adminUser' user}}{{unbound user.username}}{{/link-to}}</td>
|
||||
<td class='email'>{{unbound user.email}}</td>
|
||||
<td>{{{unbound user.last_emailed_age}}}</td>
|
||||
<td>{{{unbound user.last_seen_age}}}</td>
|
||||
<td>{{number user.topics_entered}}</td>
|
||||
<td>{{number user.posts_read_count}}</td>
|
||||
<td>{{{unbound user.time_read}}}</td>
|
||||
|
||||
<td>{{{unbound user.created_at_age}}}</td>
|
||||
|
||||
{{#if showApproval}}
|
||||
<td>
|
||||
{{#if user.approved}}
|
||||
{{i18n 'yes_value'}}
|
||||
{{else}}
|
||||
{{i18n 'no_value'}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/if}}
|
||||
<td>
|
||||
{{#if user.admin}}<i class="fa fa-shield" title="{{i18n 'admin.title'}}"></i>{{/if}}
|
||||
{{#if user.moderator}}<i class="fa fa-shield" title="{{i18n 'admin.moderator'}}"></i>{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<td>
|
||||
<a href="{{unbound user.path}}" data-user-card="{{unbound user.username}}">
|
||||
{{avatar user imageSize="small"}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{#link-to 'adminUser' user}}{{unbound user.username}}{{/link-to}}
|
||||
</td>
|
||||
<td class='email'>
|
||||
{{unbound user.email}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.last_emailed_age}}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.last_seen_age}}}
|
||||
</td>
|
||||
<td>
|
||||
{{number user.topics_entered}}
|
||||
</td>
|
||||
<td>
|
||||
{{number user.posts_read_count}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.time_read}}}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{{unbound user.created_at_age}}}
|
||||
</td>
|
||||
|
||||
{{#if showApproval}}
|
||||
<td>
|
||||
{{#if user.approved}}
|
||||
{{i18n 'yes_value'}}
|
||||
{{else}}
|
||||
{{i18n 'no_value'}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/if}}
|
||||
<td>
|
||||
{{#if user.admin}}
|
||||
{{fa-icon "shield" title="admin.title" }}
|
||||
{{/if}}
|
||||
{{#if user.moderator}}
|
||||
{{fa-icon "shield" title="admin.moderator" }}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
|
||||
@ -60,6 +60,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="web-hook-fields" args=(hash model=model)}}
|
||||
|
||||
<div>
|
||||
{{input type="checkbox" name="verify_certificate" checked=model.verify_certificate}} {{i18n 'admin.web_hooks.verify_certificate'}}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "section",
|
||||
classNameBindings: [':category-boxes-with-topics', 'anyLogos:with-logos:no-logos'],
|
||||
|
||||
@computed('categories.[].uploaded_logo.url')
|
||||
anyLogos() {
|
||||
return this.get("categories").any((c) => { return !Ember.isEmpty(c.get('uploaded_logo.url')); });
|
||||
return this.get("categories").any(c => !Ember.isEmpty(c.get('uploaded_logo.url')));
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "section",
|
||||
classNameBindings: [':category-boxes', 'anyLogos:with-logos:no-logos'],
|
||||
|
||||
@computed('categories.[].uploaded_logo.url')
|
||||
anyLogos() {
|
||||
return this.get("categories").any((c) => { return !Ember.isEmpty(c.get('uploaded_logo.url')); });
|
||||
return this.get("categories").any(c => !Ember.isEmpty(c.get('uploaded_logo.url')));
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,10 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'img',
|
||||
attributeBindings: ['cdnSrc:src'],
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
cdnSrc: function() {
|
||||
return Discourse.getURLWithCDN(this.get('src'));
|
||||
}.property('src')
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
@computed('src')
|
||||
cdnSrc(src) {
|
||||
return Discourse.getURLWithCDN(src);
|
||||
}
|
||||
});
|
||||
|
||||
@ -64,6 +64,24 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
|
||||
this.appEvents.on('post:highlight', postNumber => {
|
||||
Ember.run.scheduleOnce('afterRender', null, highlight, postNumber);
|
||||
});
|
||||
|
||||
this.appEvents.on('header:update-topic', topic => {
|
||||
|
||||
if (topic === null) {
|
||||
this._lastShowTopic = false;
|
||||
this.appEvents.trigger('header:hide-topic');
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $('html').scrollTop();
|
||||
this._lastShowTopic = this.showTopicInHeader(topic, offset);
|
||||
|
||||
if (this._lastShowTopic) {
|
||||
this.appEvents.trigger('header:show-topic', topic);
|
||||
} else {
|
||||
this.appEvents.trigger('header:hide-topic');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
const EditCategoryPanel = Ember.Component.extend({
|
||||
classNameBindings: [':modal-tab', 'activeTab::invisible'],
|
||||
});
|
||||
const EditCategoryPanel = Ember.Component.extend({});
|
||||
|
||||
export default EditCategoryPanel;
|
||||
|
||||
export function buildCategoryPanel(tab, extras) {
|
||||
return EditCategoryPanel.extend({
|
||||
activeTab: Ember.computed.equal('selectedTab', tab)
|
||||
activeTab: Ember.computed.equal('selectedTab', tab),
|
||||
classNameBindings: [':modal-tab', 'activeTab::invisible', `:edit-category-tab-${tab}`]
|
||||
}, extras || {});
|
||||
}
|
||||
|
||||
@ -5,9 +5,28 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
export default buildCategoryPanel('settings', {
|
||||
emailInEnabled: setting('email_in'),
|
||||
showPositionInput: setting('fixed_category_positions'),
|
||||
|
||||
isParentCategory: Em.computed.empty('category.parent_category_id'),
|
||||
showSubcategoryListStyle: Em.computed.and('category.show_subcategory_list', 'isParentCategory'),
|
||||
isDefaultSortOrder: Em.computed.empty('category.sort_order'),
|
||||
|
||||
@computed
|
||||
availableSubcategoryListStyles() {
|
||||
return [
|
||||
{name: I18n.t('category.subcategory_list_styles.rows'), value: 'rows'},
|
||||
{name: I18n.t('category.subcategory_list_styles.rows_with_featured_topics'), value: 'rows_with_featured_topics'},
|
||||
{name: I18n.t('category.subcategory_list_styles.boxes'), value: 'boxes'},
|
||||
{name: I18n.t('category.subcategory_list_styles.boxes_with_featured_topics'), value: 'boxes_with_featured_topics'}
|
||||
];
|
||||
},
|
||||
|
||||
@computed
|
||||
availableViews() {
|
||||
return [
|
||||
{name: I18n.t('filters.latest.title'), value: 'latest'},
|
||||
{name: I18n.t('filters.top.title'), value: 'top'}
|
||||
];
|
||||
},
|
||||
|
||||
@computed
|
||||
availableSorts() {
|
||||
return ['likes', 'op_likes', 'views', 'posts', 'activity', 'posters', 'category', 'created']
|
||||
@ -21,13 +40,5 @@ export default buildCategoryPanel('settings', {
|
||||
{name: I18n.t('category.sort_ascending'), value: 'true'},
|
||||
{name: I18n.t('category.sort_descending'), value: 'false'}
|
||||
];
|
||||
},
|
||||
|
||||
@computed
|
||||
availableViews() {
|
||||
return [
|
||||
{name: I18n.t('filters.latest.title'), value: 'latest'},
|
||||
{name: I18n.t('filters.top.title'), value: 'top'}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@ -15,37 +15,32 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed("model.is_group_user", "model.id", "groupUserIds")
|
||||
userIsGroupUser(isGroupUser, groupId, groupUserIds) {
|
||||
if (isGroupUser) {
|
||||
if (isGroupUser !== undefined) {
|
||||
return isGroupUser;
|
||||
} else {
|
||||
return !!groupUserIds && groupUserIds.includes(groupId);
|
||||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
joinGroupAction() {
|
||||
return this.currentUser ? 'joinGroup' : 'showLogin';
|
||||
},
|
||||
|
||||
@computed
|
||||
requestMembershipAction() {
|
||||
return this.currentUser ? 'requestMembership' : 'showLogin';
|
||||
_showLoginModal() {
|
||||
this.sendAction('showLogin');
|
||||
$.cookie('destination_url', window.location.href);
|
||||
},
|
||||
|
||||
actions: {
|
||||
showLogin() {
|
||||
this.sendAction('showLogin');
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
if (this.currentUser) {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
|
||||
model.addMembers(this.currentUser.get('username')).then(() => {
|
||||
model.set('is_group_user', true);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
model.addMembers(this.currentUser.get('username')).then(() => {
|
||||
model.set('is_group_user', true);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
} else {
|
||||
this._showLoginModal();
|
||||
}
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
@ -60,14 +55,18 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
requestMembership() {
|
||||
const groupName = this.get('model.name');
|
||||
if (this.currentUser) {
|
||||
const groupName = this.get('model.name');
|
||||
|
||||
Group.loadOwners(groupName).then(result => {
|
||||
const names = result.map(owner => owner.username).join(",");
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.sendAction("createNewMessageViaParams", names, title, body);
|
||||
});
|
||||
Group.loadOwners(groupName).then(result => {
|
||||
const names = result.map(owner => owner.username).join(",");
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.sendAction("createNewMessageViaParams", names, title, body);
|
||||
});
|
||||
} else {
|
||||
this._showLoginModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -14,11 +14,10 @@ const REGEXP_MIN_POST_COUNT_PREFIX = /^min_post_count:/ig;
|
||||
const REGEXP_POST_TIME_PREFIX = /^(before|after):/ig;
|
||||
const REGEXP_TAGS_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::tag\s?$)/ig;
|
||||
|
||||
|
||||
const REGEXP_IN_MATCH = /^in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig;
|
||||
const REGEXP_IN_MATCH = /^in:(posted|watching|tracking|bookmarks|first|pinned|unpinned|wiki|unseen)/ig;
|
||||
const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/ig;
|
||||
const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/ig;
|
||||
const REGEXP_SPECIAL_IN_WIKI_MATCH = /^in:wiki/ig;
|
||||
const REGEXP_SPECIAL_IN_SEEN_MATCH = /^in:seen/ig;
|
||||
|
||||
const REGEXP_CATEGORY_SLUG = /^(\#[a-zA-Z0-9\-:]+)/ig;
|
||||
const REGEXP_CATEGORY_ID = /^(category:[0-9]+)/ig;
|
||||
@ -28,6 +27,7 @@ export default Em.Component.extend({
|
||||
classNames: ['search-advanced-options'],
|
||||
|
||||
inOptions: [
|
||||
{name: I18n.t('search.advanced.filters.unseen'), value: "unseen"},
|
||||
{name: I18n.t('search.advanced.filters.posted'), value: "posted"},
|
||||
{name: I18n.t('search.advanced.filters.watching'), value: "watching"},
|
||||
{name: I18n.t('search.advanced.filters.tracking'), value: "tracking"},
|
||||
@ -35,6 +35,7 @@ export default Em.Component.extend({
|
||||
{name: I18n.t('search.advanced.filters.first'), value: "first"},
|
||||
{name: I18n.t('search.advanced.filters.pinned'), value: "pinned"},
|
||||
{name: I18n.t('search.advanced.filters.unpinned'), value: "unpinned"},
|
||||
{name: I18n.t('search.advanced.filters.wiki'), value: "wiki"},
|
||||
],
|
||||
statusOptions: [
|
||||
{name: I18n.t('search.advanced.statuses.open'), value: "open"},
|
||||
@ -75,7 +76,7 @@ export default Em.Component.extend({
|
||||
in: {
|
||||
likes: false,
|
||||
private: false,
|
||||
wiki: false
|
||||
seen: false
|
||||
}
|
||||
},
|
||||
status: '',
|
||||
@ -102,7 +103,7 @@ export default Em.Component.extend({
|
||||
this.setSearchedTermValue('searchedTerms.in', REGEXP_IN_PREFIX, REGEXP_IN_MATCH);
|
||||
this.setSearchedTermSpecialInValue('searchedTerms.special.in.likes', REGEXP_SPECIAL_IN_LIKES_MATCH);
|
||||
this.setSearchedTermSpecialInValue('searchedTerms.special.in.private', REGEXP_SPECIAL_IN_PRIVATE_MATCH);
|
||||
this.setSearchedTermSpecialInValue('searchedTerms.special.in.wiki', REGEXP_SPECIAL_IN_WIKI_MATCH);
|
||||
this.setSearchedTermSpecialInValue('searchedTerms.special.in.seen', REGEXP_SPECIAL_IN_SEEN_MATCH);
|
||||
this.setSearchedTermValue('searchedTerms.status', REGEXP_STATUS_PREFIX);
|
||||
this.setSearchedTermValueForPostTime();
|
||||
this.setSearchedTermValue('searchedTerms.min_post_count', REGEXP_MIN_POST_COUNT_PREFIX);
|
||||
@ -438,15 +439,15 @@ export default Em.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@observes('searchedTerms.special.in.wiki')
|
||||
updateSearchTermForSpecialInWiki() {
|
||||
const match = this.filterBlocks(REGEXP_SPECIAL_IN_WIKI_MATCH);
|
||||
const inFilter = this.get('searchedTerms.special.in.wiki');
|
||||
@observes('searchedTerms.special.in.seen')
|
||||
updateSearchTermForSpecialInSeen() {
|
||||
const match = this.filterBlocks(REGEXP_SPECIAL_IN_SEEN_MATCH);
|
||||
const inFilter = this.get('searchedTerms.special.in.seen');
|
||||
let searchTerm = this.get('searchTerm') || '';
|
||||
|
||||
if (inFilter) {
|
||||
if (match.length === 0) {
|
||||
searchTerm += ` in:wiki`;
|
||||
searchTerm += ` in:seen`;
|
||||
this.set('searchTerm', searchTerm.trim());
|
||||
}
|
||||
} else if (match.length !== 0) {
|
||||
|
||||
@ -116,7 +116,6 @@ export default Ember.Component.extend(CleansUp, {
|
||||
|
||||
const args = { stats: false };
|
||||
args.include_post_count_for = this.get('topic.id');
|
||||
args.skip_track_visit = true;
|
||||
|
||||
User.findByUsername(username, args).then(user => {
|
||||
if (user.topic_post_count) {
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import DiscoveryController from 'discourse/controllers/discovery';
|
||||
|
||||
const subcategoryStyleComponentNames = {
|
||||
'rows': 'categories_only',
|
||||
'rows_with_featured_topics': 'categories_with_featured_topics',
|
||||
'boxes': 'categories_boxes',
|
||||
'boxes_with_featured_topics': 'categories_boxes_with_topics'
|
||||
};
|
||||
|
||||
export default DiscoveryController.extend({
|
||||
discovery: Ember.inject.controller(),
|
||||
|
||||
@ -19,7 +26,12 @@ export default DiscoveryController.extend({
|
||||
|
||||
@computed("model.parentCategory")
|
||||
categoryPageStyle(parentCategory) {
|
||||
const style = this.siteSettings.desktop_category_page_style;
|
||||
let style = this.siteSettings.desktop_category_page_style;
|
||||
|
||||
if (parentCategory) {
|
||||
style = subcategoryStyleComponentNames[parentCategory.get('subcategory_list_style')] || style;
|
||||
}
|
||||
|
||||
const componentName = (parentCategory && style === "categories_and_latest_topics") ?
|
||||
"categories_only" :
|
||||
style;
|
||||
|
||||
@ -8,6 +8,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
saving: false,
|
||||
deleting: false,
|
||||
panels: null,
|
||||
hiddenTooltip: true,
|
||||
|
||||
_initPanels: function() {
|
||||
this.set('panels', []);
|
||||
@ -16,6 +17,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
onShow() {
|
||||
this.changeSize();
|
||||
this.titleChanged();
|
||||
this.set('hiddenTooltip', true);
|
||||
},
|
||||
|
||||
changeSize: function() {
|
||||
@ -101,6 +103,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
self.set('deleting', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleDeleteTooltip() {
|
||||
this.toggleProperty('hiddenTooltip');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,18 +53,18 @@ export default Ember.Controller.extend({
|
||||
this.get('tabs')[0].set('count', this.get('model.user_count'));
|
||||
},
|
||||
|
||||
@computed('model.is_group_user', 'model.is_group_owner', 'model.automatic')
|
||||
getTabs(isGroupUser, isGroupOwner, automatic) {
|
||||
@computed('model.is_group_owner', 'model.automatic')
|
||||
getTabs() {
|
||||
return this.get('tabs').filter(t => {
|
||||
let display = true;
|
||||
let canSee = true;
|
||||
|
||||
if (this.currentUser && t.get('requiresGroupAdmin')) {
|
||||
display = automatic ? false : (this.currentUser.admin || isGroupOwner);
|
||||
} else if (t.get('requiresGroupAdmin')) {
|
||||
display = false;
|
||||
if (this.currentUser && t.requiresGroupAdmin) {
|
||||
canSee = this.currentUser.canManageGroup(this.get('model'));
|
||||
} else if (t.requiresGroupAdmin) {
|
||||
canSee = false;
|
||||
}
|
||||
|
||||
return display;
|
||||
return canSee;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,15 +5,39 @@ import IncomingEmail from 'admin/models/incoming-email';
|
||||
// This controller handles displaying of raw email
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
rawEmail: "",
|
||||
textPart: "",
|
||||
htmlPart: "",
|
||||
|
||||
tab: "raw",
|
||||
|
||||
showRawEmail: Ember.computed.equal("tab", "raw"),
|
||||
showTextPart: Ember.computed.equal("tab", "text_part"),
|
||||
showHtmlPart: Ember.computed.equal("tab", "html_part"),
|
||||
|
||||
onShow() { this.send("displayRaw"); },
|
||||
|
||||
loadRawEmail(postId) {
|
||||
return Post.loadRawEmail(postId)
|
||||
.then(result => this.set("rawEmail", result.raw_email));
|
||||
.then(result => this.setProperties({
|
||||
"rawEmail": result.raw_email,
|
||||
"textPart": result.text_part,
|
||||
"htmlPart": result.html_part,
|
||||
}));
|
||||
},
|
||||
|
||||
loadIncomingRawEmail(incomingEmailId) {
|
||||
return IncomingEmail.loadRawEmail(incomingEmailId)
|
||||
.then(result => this.set("rawEmail", result.raw_email));
|
||||
.then(result => this.setProperties({
|
||||
"rawEmail": result.raw_email,
|
||||
"textPart": result.text_part,
|
||||
"htmlPart": result.html_part,
|
||||
}));
|
||||
},
|
||||
|
||||
actions: {
|
||||
displayRaw() { this.set("tab", "raw"); },
|
||||
displayTextPart() { this.set("tab", "text_part"); },
|
||||
displayHtmlPart() { this.set("tab", "html_part"); }
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -913,7 +913,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
if (data.reload_topic) {
|
||||
topic.reload().then(() => {
|
||||
this.send('postChangedRoute', topic.get('post_number') || 1);
|
||||
this.appEvents.trigger('header:show-topic', topic);
|
||||
this.appEvents.trigger('header:update-topic', topic);
|
||||
});
|
||||
} else {
|
||||
if (topic.get('isPrivateMessage') &&
|
||||
|
||||
@ -10,13 +10,9 @@ export default {
|
||||
after: 'message-bus',
|
||||
|
||||
initialize(container) {
|
||||
const user = container.lookup('current-user:main'),
|
||||
site = container.lookup('site:main'),
|
||||
siteSettings = container.lookup('site-settings:main'),
|
||||
bus = container.lookup('message-bus:main'),
|
||||
keyValueStore = container.lookup('key-value-store:main'),
|
||||
store = container.lookup('store:main'),
|
||||
appEvents = container.lookup('app-events:main');
|
||||
const user = container.lookup('current-user:main');
|
||||
const keyValueStore = container.lookup('key-value-store:main');
|
||||
const bus = container.lookup('message-bus:main');
|
||||
|
||||
// clear old cached notifications, we used to store in local storage
|
||||
// TODO 2017 delete this line
|
||||
@ -36,12 +32,17 @@ export default {
|
||||
}
|
||||
|
||||
bus.subscribe(`/notification/${user.get('id')}`, data => {
|
||||
const store = container.lookup('store:main');
|
||||
const appEvents = container.lookup('app-events:main');
|
||||
|
||||
const oldUnread = user.get('unread_notifications');
|
||||
const oldPM = user.get('unread_private_messages');
|
||||
|
||||
user.set('unread_notifications', data.unread_notifications);
|
||||
user.set('unread_private_messages', data.unread_private_messages);
|
||||
user.set('read_first_notification', data.read_first_notification);
|
||||
user.setProperties({
|
||||
unread_notifications: data.unread_notifications,
|
||||
unread_private_messages: data.unread_private_messages,
|
||||
read_first_notification: data.read_first_notification
|
||||
});
|
||||
|
||||
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
|
||||
appEvents.trigger('notifications:changed');
|
||||
@ -84,6 +85,9 @@ export default {
|
||||
}
|
||||
}, user.notification_channel_position);
|
||||
|
||||
const site = container.lookup('site:main');
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
|
||||
bus.subscribe("/categories", data => {
|
||||
_.each(data.categories, c => site.updateCategory(c));
|
||||
_.each(data.deleted_categories, id => site.removeCategory(id));
|
||||
|
||||
@ -69,7 +69,7 @@ export function ajax() {
|
||||
args.error = (xhr, textStatus, errorThrown) => {
|
||||
// note: for bad CSRF we don't loop an extra request right away.
|
||||
// this allows us to eliminate the possibility of having a loop.
|
||||
if (xhr.status === 403 && xhr.responseText === "['BAD CSRF']") {
|
||||
if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") {
|
||||
Discourse.Session.current().set('csrfToken', null);
|
||||
}
|
||||
|
||||
|
||||
@ -261,7 +261,11 @@ export default function(options) {
|
||||
left: "-1000px"
|
||||
});
|
||||
|
||||
me.parent().append(div);
|
||||
if (options.appendSelector) {
|
||||
me.parents(options.appendSelector).append(div);
|
||||
} else {
|
||||
me.parent().append(div);
|
||||
}
|
||||
|
||||
if (!isInput && !options.treatAsTextarea) {
|
||||
vOffset = div.height();
|
||||
|
||||
@ -46,12 +46,16 @@ export default {
|
||||
|
||||
// Update badge clicks unless it's our own
|
||||
if (!ownLink) {
|
||||
var $badge = $('span.badge', $link);
|
||||
const $badge = $('span.badge', $link);
|
||||
if ($badge.length === 1) {
|
||||
// don't update counts in category badge nor in oneboxes (except when we force it)
|
||||
if (isValidLink($link)) {
|
||||
var html = $badge.html();
|
||||
if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
|
||||
const html = $badge.html();
|
||||
const key = `${new Date().toLocaleDateString()}-${postId}-${href}`;
|
||||
if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) {
|
||||
sessionStorage.setItem(key, true);
|
||||
$badge.html(parseInt(html, 10) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,14 +130,14 @@ export function isValidSearchTerm(searchTerm) {
|
||||
}
|
||||
};
|
||||
|
||||
export function applySearchAutocomplete($input, siteSettings, appEvents) {
|
||||
export function applySearchAutocomplete($input, siteSettings, appEvents, options) {
|
||||
const afterComplete = function() {
|
||||
if (appEvents) {
|
||||
appEvents.trigger("search-autocomplete:after-complete");
|
||||
}
|
||||
};
|
||||
|
||||
$input.autocomplete({
|
||||
$input.autocomplete(_.merge({
|
||||
template: findRawTemplate('category-tag-autocomplete'),
|
||||
key: '#',
|
||||
width: '100%',
|
||||
@ -153,9 +153,9 @@ export function applySearchAutocomplete($input, siteSettings, appEvents) {
|
||||
return searchCategoryTag(term, siteSettings);
|
||||
},
|
||||
afterComplete
|
||||
});
|
||||
}, options));
|
||||
|
||||
$input.autocomplete({
|
||||
$input.autocomplete(_.merge({
|
||||
template: findRawTemplate('user-selector-autocomplete'),
|
||||
key: "@",
|
||||
width: '100%',
|
||||
@ -163,5 +163,5 @@ export function applySearchAutocomplete($input, siteSettings, appEvents) {
|
||||
transformComplete: v => v.username || v.name,
|
||||
dataSource: term => userSearch({ term, includeGroups: true }),
|
||||
afterComplete
|
||||
});
|
||||
}, options));
|
||||
};
|
||||
|
||||
@ -105,7 +105,8 @@ const Category = RestModel.extend({
|
||||
topic_featured_link_allowed: this.get('topic_featured_link_allowed'),
|
||||
show_subcategory_list: this.get('show_subcategory_list'),
|
||||
num_featured_topics: this.get('num_featured_topics'),
|
||||
default_view: this.get('default_view')
|
||||
default_view: this.get('default_view'),
|
||||
subcategory_list_style: this.get('subcategory_list_style')
|
||||
},
|
||||
type: id ? 'PUT' : 'POST'
|
||||
});
|
||||
|
||||
@ -17,9 +17,10 @@ const Group = RestModel.extend({
|
||||
return Em.isEmpty(value) ? "" : value;
|
||||
},
|
||||
|
||||
type: function() {
|
||||
return this.get("automatic") ? "automatic" : "custom";
|
||||
}.property("automatic"),
|
||||
@computed('automatic')
|
||||
type(automatic) {
|
||||
return automatic ? "automatic" : "custom";
|
||||
},
|
||||
|
||||
@computed('user_count')
|
||||
userCountDisplay(userCount) {
|
||||
@ -93,6 +94,7 @@ const Group = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@computed('flair_bg_color')
|
||||
flairBackgroundHexColor() {
|
||||
return this.get('flair_bg_color') ? this.get('flair_bg_color').replace(new RegExp("[^0-9a-fA-F]", "g"), "") : null;
|
||||
@ -224,7 +226,7 @@ Group.reopenClass({
|
||||
|
||||
mentionable(name) {
|
||||
return ajax(`/groups/${name}/mentionable`, { data: { name } });
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default Group;
|
||||
|
||||
@ -500,8 +500,11 @@ const User = RestModel.extend({
|
||||
|
||||
return summary;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
canManageGroup(group) {
|
||||
return group.get('automatic') ? false : (this.get('admin') || group.get('is_group_owner'));
|
||||
}
|
||||
});
|
||||
|
||||
User.reopenClass(Singleton, {
|
||||
|
||||
@ -7,6 +7,12 @@ export default Ember.Route.extend({
|
||||
return this.modelFor('group');
|
||||
},
|
||||
|
||||
afterModel(group) {
|
||||
if (!this.currentUser || !this.currentUser.canManageGroup(group)) {
|
||||
this.transitionTo("group.members", group);
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this.controllerFor('group-edit').setProperties({ model });
|
||||
this.controllerFor("group").set("showing", 'edit');
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
{{#each categories as |c|}}
|
||||
<div class='category-box category-box-{{unbound c.slug}}' style={{border-color c.color}}>
|
||||
<div class='category-box-inner'>
|
||||
<div class='category-box-heading'>
|
||||
<a href={{c.url}}>
|
||||
{{#if c.uploaded_logo.url}}
|
||||
{{cdn-img src=c.uploaded_logo.url class="logo"}}
|
||||
{{/if}}
|
||||
|
||||
<h3>{{c.name}}</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='featured-topics'>
|
||||
{{#if c.topics}}
|
||||
<ul>
|
||||
{{#each c.topics as |topic|}}
|
||||
<li>
|
||||
<a class='title' href="{{unbound topic.lastUnreadUrl}}">
|
||||
{{text-overflow class="overflow" text=topic.fancyTitle}}
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
@ -0,0 +1,16 @@
|
||||
{{#each categories as |c|}}
|
||||
<div class='category-box'>
|
||||
<a href={{c.url}}>
|
||||
{{#if c.uploaded_logo.url}}
|
||||
{{cdn-img src=c.uploaded_logo.url class="logo"}}
|
||||
{{/if}}
|
||||
|
||||
<div class='details'>
|
||||
<h3>{{c.name}}</h3>
|
||||
<div class='description'>
|
||||
{{{c.description_excerpt}}}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{/each}}
|
||||
@ -0,0 +1,3 @@
|
||||
{{#if src}}
|
||||
<img src={{cdnSrc}} class={{class}}>
|
||||
{{/if}}
|
||||
@ -4,7 +4,7 @@
|
||||
imageId=logoImageId
|
||||
imageUrl=logoImageUrl
|
||||
type="category_logo"
|
||||
class="no-repeat"}}
|
||||
class="no-repeat contain-image"}}
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
|
||||
@ -19,14 +19,51 @@
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{#unless category.parent_category_id}}
|
||||
<section class="field">
|
||||
{{#if isParentCategory}}
|
||||
<section class="field show-subcategory-list-field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.show_subcategory_list}}
|
||||
{{i18n "category.show_subcategory_list"}}
|
||||
</label>
|
||||
</section>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showSubcategoryListStyle}}
|
||||
<section class="field subcategory-list-style-field">
|
||||
<label>
|
||||
{{i18n "category.subcategory_list_style"}}
|
||||
{{combo-box valueAttribute="value" content=availableSubcategoryListStyles value=category.subcategory_list_style}}
|
||||
</label>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field default-view-field">
|
||||
<label>
|
||||
{{i18n "category.default_view"}}
|
||||
{{combo-box valueAttribute="value" content=availableViews value=category.default_view}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{i18n "category.sort_order"}}
|
||||
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
|
||||
{{#unless isDefaultSortOrder}}
|
||||
{{combo-box valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
|
||||
{{/unless}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field num-featured-topics-fields">
|
||||
<label>
|
||||
{{#if category.parent_category_id}}
|
||||
{{i18n "category.subcategory_num_featured_topics"}}
|
||||
{{else}}
|
||||
{{i18n "category.num_featured_topics"}}
|
||||
{{/if}}
|
||||
{{text-field value=category.num_featured_topics}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
@ -46,23 +83,6 @@
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{i18n "category.sort_order"}}
|
||||
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
|
||||
{{#unless isDefaultSortOrder}}
|
||||
{{combo-box valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
|
||||
{{/unless}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field default-view-field">
|
||||
<label>
|
||||
{{i18n "category.default_view"}}
|
||||
{{combo-box valueAttribute="value" content=availableViews value=category.default_view}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{#if emailInEnabled}}
|
||||
<section class='field'>
|
||||
<label>
|
||||
@ -82,17 +102,6 @@
|
||||
{{plugin-outlet name="category-email-in" args=(hash category=category)}}
|
||||
{{/if}}
|
||||
|
||||
<section class="field num-featured-topics-fields">
|
||||
<label>
|
||||
{{#if category.parent_category_id}}
|
||||
{{i18n "category.subcategory_num_featured_topics"}}
|
||||
{{else}}
|
||||
{{i18n "category.num_featured_topics"}}
|
||||
{{/if}}
|
||||
{{text-field value=category.num_featured_topics}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{#if showPositionInput}}
|
||||
<section class='field position-fields'>
|
||||
<label>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
label="groups.leave"
|
||||
disabled=updatingMembership}}
|
||||
{{else}}
|
||||
{{d-button action=joinGroupAction
|
||||
{{d-button action="joinGroup"
|
||||
class="group-index-join"
|
||||
icon="plus"
|
||||
label="groups.join"
|
||||
@ -22,7 +22,7 @@
|
||||
disabled=true}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{d-button action=requestMembershipAction
|
||||
{{d-button action="requestMembership"
|
||||
class="group-index-request"
|
||||
icon="envelope"
|
||||
label="groups.request"}}
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
<section class='field'>
|
||||
<label>{{input type="checkbox" class="in-likes" checked=searchedTerms.special.in.likes}} {{i18n "search.advanced.filters.likes"}}</label>
|
||||
<label>{{input type="checkbox" class="in-private" checked=searchedTerms.special.in.private}} {{i18n "search.advanced.filters.private"}}</label>
|
||||
<label>{{input type="checkbox" class="in-wiki" checked=searchedTerms.special.in.wiki}} {{i18n "search.advanced.filters.wiki"}}</label>
|
||||
<label>{{input type="checkbox" class="in-seen" checked=searchedTerms.special.in.seen}} {{i18n "search.advanced.filters.seen"}}</label>
|
||||
</section>
|
||||
{{combo-box id="in" valueAttribute="value" content=inOptions value=searchedTerms.in none="user.locale.any"}}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
{{group-membership-button model=model
|
||||
createNewMessageViaParams='createNewMessageViaParams'
|
||||
showLogin='showLogin'}}
|
||||
|
||||
{{#if hasMembers}}
|
||||
{{#load-more selector=".group-members tr" action="loadMore"}}
|
||||
<table class='group-members'>
|
||||
|
||||
@ -29,17 +29,25 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=application.currentPath}}
|
||||
{{#each getTabs as |tab|}}
|
||||
<li>
|
||||
{{#link-to tab.location model title=tab.message}}
|
||||
{{#if tab.icon}}{{fa-icon tab.icon}}{{/if}}
|
||||
{{tab.message}}
|
||||
{{#if tab.count}}<span class='count'>({{tab.count}})</span>{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/mobile-nav}}
|
||||
<div class="list-controls">
|
||||
<div class="container">
|
||||
{{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=application.currentPath}}
|
||||
{{#each getTabs as |tab|}}
|
||||
<li>
|
||||
{{#link-to tab.location model title=tab.message}}
|
||||
{{#if tab.icon}}{{fa-icon tab.icon}}{{/if}}
|
||||
{{tab.message}}
|
||||
{{#if tab.count}}<span class='count'>({{tab.count}})</span>{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/mobile-nav}}
|
||||
|
||||
{{group-membership-button model=model
|
||||
createNewMessageViaParams='createNewMessageViaParams'
|
||||
showLogin='showLogin'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='group-outlet'>
|
||||
{{outlet}}
|
||||
|
||||
@ -28,8 +28,16 @@
|
||||
icon="trash-o"
|
||||
label="category.delete"}}
|
||||
{{else}}
|
||||
<div class="cannot_delete_reason">
|
||||
{{{model.cannot_delete_reason}}}
|
||||
<div class="disable_info_wrap">
|
||||
{{d-button disabled=deleteDisabled
|
||||
class="disable-no-hover"
|
||||
action="toggleDeleteTooltip"
|
||||
icon="question-circle"
|
||||
label="category.delete"}}
|
||||
|
||||
<div class="cannot_delete_reason {{if hiddenTooltip "hidden" ""}}">
|
||||
{{{model.cannot_delete_reason}}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,45 @@
|
||||
{{#d-modal-body title="raw_email.title" maxHeight="80%"}}
|
||||
{{#if rawEmail}}
|
||||
{{textarea value=rawEmail class="raw-email-textarea"}}
|
||||
{{else}}
|
||||
{{i18n 'raw_email.not_available'}}
|
||||
{{/if}}
|
||||
{{#d-modal-body title="raw_email.title" class="incoming-email-modal" maxHeight="80%"}}
|
||||
<div class="incoming-email-tabs">
|
||||
{{d-button action="displayRaw"
|
||||
label="post.raw_email.displays.raw.button"
|
||||
title="post.raw_email.displays.raw.title"
|
||||
class=(if showRawEmail 'active')
|
||||
}}
|
||||
|
||||
{{#if textPart}}
|
||||
{{d-button action="displayTextPart"
|
||||
label="post.raw_email.displays.text_part.button"
|
||||
title="post.raw_email.displays.text_part.title"
|
||||
class=(if showTextPart 'active')
|
||||
}}
|
||||
{{/if}}
|
||||
|
||||
{{#if htmlPart}}
|
||||
{{d-button action="displayHtmlPart"
|
||||
label="post.raw_email.displays.html_part.button"
|
||||
title="post.raw_email.displays.html_part.title"
|
||||
class=(if showHtmlPart 'active')
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="incoming-email-content">
|
||||
{{#if showRawEmail}}
|
||||
{{#if rawEmail}}
|
||||
{{textarea value=rawEmail}}
|
||||
{{else}}
|
||||
{{i18n 'raw_email.not_available'}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showTextPart}}
|
||||
{{textarea value=textPart}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showHtmlPart}}
|
||||
<div class="incoming-email-html-part">
|
||||
{{{htmlPart}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{{#d-section pageClass="user-preferences" class="user-content user-preferences"}}
|
||||
|
||||
{{plugin-outlet name="above-user-preferences"}}
|
||||
|
||||
<form class="form-horizontal">
|
||||
|
||||
<div class="control-group save-button" id='save-button-top'>
|
||||
|
||||
@ -6,6 +6,12 @@ createWidget('hamburger-category', {
|
||||
tagName: 'li.category-link',
|
||||
|
||||
html(c) {
|
||||
if (c.parent_category_id) {
|
||||
this.tagName += '.subcategory';
|
||||
}
|
||||
|
||||
this.tagName += '.category-' + Discourse.Category.slugFor(c, '-');
|
||||
|
||||
const results = [ this.attach('category-link', { category: c, allowUncategorized: true }) ];
|
||||
|
||||
const unreadTotal = parseInt(c.get('unreadTopics'), 10) + parseInt(c.get('newTopics'), 10);
|
||||
|
||||
@ -262,7 +262,10 @@ export default createWidget('header', {
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
const $searchInput = $('#search-term');
|
||||
$searchInput.focus().select();
|
||||
applySearchAutocomplete($searchInput, this.siteSettings, this.appEvents);
|
||||
|
||||
applySearchAutocomplete($searchInput, this.siteSettings, this.appEvents, {
|
||||
appendSelector: '.menu-panel'
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -16,6 +16,8 @@ const icons = {
|
||||
'pinned.disabled': 'thumb-tack unpinned',
|
||||
'pinned_globally.enabled': 'thumb-tack',
|
||||
'pinned_globally.disabled': 'thumb-tack unpinned',
|
||||
'banner.enabled': 'thumb-tack',
|
||||
'banner.disabled': 'thumb-tack unpinned',
|
||||
'visible.enabled': 'eye',
|
||||
'visible.disabled': 'eye-slash',
|
||||
'split_topic': 'sign-out',
|
||||
|
||||
@ -137,10 +137,13 @@ export default createWidget('topic-admin-menu', {
|
||||
icon: 'thumb-tack',
|
||||
label: featured ? 'actions.unpin' : 'actions.pin' });
|
||||
}
|
||||
buttons.push({ className: 'topic-admin-change-timestamp',
|
||||
action: 'showChangeTimestamp',
|
||||
icon: 'calendar',
|
||||
label: 'change_timestamp.title' });
|
||||
|
||||
if (this.currentUser.admin) {
|
||||
buttons.push({ className: 'topic-admin-change-timestamp',
|
||||
action: 'showChangeTimestamp',
|
||||
icon: 'calendar',
|
||||
label: 'change_timestamp.title' });
|
||||
}
|
||||
|
||||
if (!isPrivateMessage) {
|
||||
buttons.push({ className: 'topic-admin-archive',
|
||||
|
||||
@ -31,8 +31,10 @@ $mobile-breakpoint: 700px;
|
||||
width: 100%;
|
||||
tr {text-align: left;}
|
||||
td, th {padding: 8px;}
|
||||
th {border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);}
|
||||
th {border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); text-align: left;}
|
||||
td {border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);}
|
||||
th.sortable i.fa-chevron-down,
|
||||
th.sortable i.fa-chevron-up {margin-left: 0.5em;}
|
||||
tr:hover { background-color: darken($secondary, 2.5%); }
|
||||
tr.selected { background-color: lighten($primary, 80%); }
|
||||
.filters input { margin-bottom: 0; }
|
||||
@ -696,6 +698,10 @@ section.details {
|
||||
width: 100%;
|
||||
border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
||||
}
|
||||
|
||||
.content-list {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Customise area
|
||||
|
||||
@ -73,9 +73,6 @@
|
||||
max-width: 710px;
|
||||
margin: 0 auto;
|
||||
background-color: $secondary;
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
|
||||
box-shadow: 0 3px 7px rgba(0,0,0, .8);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
@ -154,9 +151,6 @@
|
||||
.warning {
|
||||
color: $danger !important;
|
||||
}
|
||||
.raw-email-textarea {
|
||||
height: 300px;
|
||||
}
|
||||
.json-uploader {
|
||||
.jsfu-shade-container {
|
||||
display: table-row;
|
||||
@ -328,5 +322,79 @@
|
||||
.auto-close-fields label {
|
||||
font-size: .929em;
|
||||
}
|
||||
|
||||
.subcategory-list-style-field {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.edit-category-tab-settings {
|
||||
section.field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.disable_info_wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
|
||||
.cannot_delete_reason {
|
||||
position: absolute;
|
||||
background: dark-light-choose(scale-color($primary, $lightness: 10%), scale-color($secondary, $lightness: 10%));
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 100%), scale-color($secondary, $lightness: 0%));
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 12px 8px;
|
||||
|
||||
&::after {
|
||||
top: 100%;
|
||||
left: 57%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
border-top-color: dark-light-choose(scale-color($primary, $lightness: 10%), scale-color($secondary, $lightness: 10%));
|
||||
border-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.incoming-email-modal {
|
||||
.btn {
|
||||
transition: none;
|
||||
background-color: transparent;
|
||||
margin-right: 5px;
|
||||
&:hover, &.active {
|
||||
color: $primary;
|
||||
}
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
&:focus {
|
||||
outline: 2px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
}
|
||||
}
|
||||
.incoming-email-tabs {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.incoming-email-content {
|
||||
height: 300px;
|
||||
textarea, .incoming-email-html-part {
|
||||
height: 95%;
|
||||
border: none;
|
||||
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
padding-top: 10px;
|
||||
}
|
||||
textarea {
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
border-radius: 0px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.incoming-email-html-part {
|
||||
padding: 10px 4px 4px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@ aside.quote {
|
||||
color: dark-light-choose($secondary, $primary);
|
||||
padding: 10px;
|
||||
z-index: 401;
|
||||
opacity: 0.8;
|
||||
opacity: 0.9;
|
||||
|
||||
&:before {
|
||||
font-family: "FontAwesome";
|
||||
|
||||
@ -8,3 +8,9 @@
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.image-uploader.contain-image {
|
||||
.uploaded-image-preview {
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,3 +76,11 @@
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
// Buttons
|
||||
// ---------------------------------------------------
|
||||
.disable-no-hover:hover {
|
||||
background: dark-light-choose(scale-color($primary, $lightness: 90%), scale-color($secondary, $lightness: 60%));;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
@ -176,3 +176,126 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-boxes, .category-boxes-with-topics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
|
||||
.category-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: flex-start;
|
||||
|
||||
box-sizing: border-box;
|
||||
border: 2px solid blend-primary-secondary(20%);
|
||||
|
||||
.mobile-view & {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.details {
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.description {
|
||||
font-size: 1.05em;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 40%));
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
height: 40px;
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-logos {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-boxes {
|
||||
.category-box {
|
||||
width: 23%;
|
||||
margin: 0 1% 1.5em 1%;
|
||||
}
|
||||
.details {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&.no-logos {
|
||||
.category-box a {
|
||||
padding: 3em 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-boxes-with-topics {
|
||||
.category-box {
|
||||
width: 31%;
|
||||
margin: 0 1% 1.5em 1%;
|
||||
padding: 0;
|
||||
border-width: 0 0 0 6px;
|
||||
border-style: solid;
|
||||
border-color: blend-primary-secondary(20%);
|
||||
}
|
||||
.category-box-inner {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border-width: 2px 2px 2px 0;
|
||||
border-style: solid;
|
||||
border-color: blend-primary-secondary(20%);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-box-heading {
|
||||
padding: 1em 1em 0.5em 1em;
|
||||
a[href] {
|
||||
width: 100%;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.featured-topics {
|
||||
padding: 0.5em 1em 1em 1em;
|
||||
ul {
|
||||
color: blend-primary-secondary(70%);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
li {
|
||||
padding: 0;
|
||||
margin-left: 1.5em;
|
||||
.overflow {
|
||||
max-height: 3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
li:before {
|
||||
content: '\f0f6';
|
||||
font-family: 'FontAwesome';
|
||||
float: left;
|
||||
margin-left: -1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,11 +117,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cannot_delete_reason {
|
||||
float: right;
|
||||
text-align: right;
|
||||
max-width: 380px;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%));
|
||||
.disable_info_wrap {
|
||||
margin-top: -70px;
|
||||
padding-top: 70px;
|
||||
@media all and (min-width: 550px) {
|
||||
padding-left: 170px;
|
||||
}
|
||||
|
||||
.cannot_delete_reason {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
max-width: 380px;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
|
||||
.group-nav.mobile-nav {
|
||||
margin-bottom: 15px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.group-activity {
|
||||
@ -29,7 +30,7 @@
|
||||
.group-activity-nav.mobile-nav {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -50px;
|
||||
top: -55px;
|
||||
}
|
||||
|
||||
.group-activity-outlet {
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
// we need a little extra room on mobile for the
|
||||
// we need a little extra room on mobile for the
|
||||
// stuff inside the footer to fit
|
||||
.modal-footer {
|
||||
padding-right: 0;
|
||||
@ -133,6 +133,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disable_info_wrap .cannot_delete_reason {
|
||||
top: -114px;
|
||||
right: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabbed-modal {
|
||||
|
||||
@ -78,7 +78,8 @@ class Admin::EmailController < Admin::AdminController
|
||||
def raw_email
|
||||
params.require(:id)
|
||||
incoming_email = IncomingEmail.find(params[:id].to_i)
|
||||
render json: { raw_email: incoming_email.raw }
|
||||
text, html = Email.extract_parts(incoming_email.raw)
|
||||
render json: { raw_email: incoming_email.raw, text_part: text, html_part: html }
|
||||
end
|
||||
|
||||
def incoming
|
||||
|
||||
@ -14,25 +14,50 @@ class Admin::EmojisController < Admin::AdminController
|
||||
.gsub(/_{2,}/, '_')
|
||||
.downcase
|
||||
|
||||
data = if Emoji.exists?(name)
|
||||
failed_json.merge(errors: [I18n.t("emoji.errors.name_already_exists", name: name)])
|
||||
elsif emoji = Emoji.create_for(file, name)
|
||||
emoji
|
||||
else
|
||||
failed_json.merge(errors: [I18n.t("emoji.errors.error_while_storing_emoji")])
|
||||
end
|
||||
upload = Upload.create_for(
|
||||
current_user.id,
|
||||
file.tempfile,
|
||||
file.original_filename,
|
||||
File.size(file.tempfile.path),
|
||||
image_type: 'custom_emoji'
|
||||
)
|
||||
|
||||
data =
|
||||
if upload.persisted?
|
||||
custom_emoji = CustomEmoji.new(name: name, upload: upload)
|
||||
|
||||
if custom_emoji.save
|
||||
Emoji.clear_cache
|
||||
{ name: custom_emoji.name, url: custom_emoji.upload.url }
|
||||
else
|
||||
failed_json.merge(errors: custom_emoji.errors.full_messages)
|
||||
end
|
||||
else
|
||||
failed_json.merge(errors: upload.errors.full_messages)
|
||||
end
|
||||
|
||||
MessageBus.publish("/uploads/emoji", data.as_json, user_ids: [current_user.id])
|
||||
end
|
||||
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def destroy
|
||||
name = params.require(:id)
|
||||
Emoji[name].try(:remove)
|
||||
render nothing: true
|
||||
|
||||
custom_emoji = CustomEmoji.find_by(name: name)
|
||||
raise Discourse::InvalidParameters unless custom_emoji
|
||||
|
||||
CustomEmoji.transaction do
|
||||
custom_emoji.upload.destroy!
|
||||
custom_emoji.destroy!
|
||||
end
|
||||
|
||||
Emoji.clear_cache
|
||||
|
||||
Jobs.enqueue(:rebake_custom_emoji_posts, name: name)
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -29,7 +29,7 @@ class ApplicationController < ActionController::Base
|
||||
unless is_api? || is_user_api?
|
||||
super
|
||||
clear_current_user
|
||||
render text: "['BAD CSRF']", status: 403
|
||||
render text: "[\"BAD CSRF\"]", status: 403
|
||||
end
|
||||
end
|
||||
|
||||
@ -230,8 +230,8 @@ class ApplicationController < ActionController::Base
|
||||
preload_anonymous_data
|
||||
|
||||
if current_user
|
||||
preload_current_user_data
|
||||
current_user.sync_notification_channel_position
|
||||
preload_current_user_data
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -16,10 +16,12 @@ class CategoriesController < ApplicationController
|
||||
|
||||
@description = SiteSetting.site_description
|
||||
|
||||
parent_category = Category.find_by(slug: params[:parent_category_id]) || Category.find_by(id: params[:parent_category_id].to_i)
|
||||
|
||||
category_options = {
|
||||
is_homepage: current_homepage == "categories".freeze,
|
||||
parent_category_id: params[:parent_category_id],
|
||||
include_topics: include_topics
|
||||
include_topics: include_topics(parent_category)
|
||||
}
|
||||
|
||||
@category_list = CategoryList.new(guardian, category_options)
|
||||
@ -57,7 +59,8 @@ class CategoriesController < ApplicationController
|
||||
|
||||
topic_options = {
|
||||
per_page: SiteSetting.categories_topics,
|
||||
no_definitions: true
|
||||
no_definitions: true,
|
||||
exclude_category_ids: Category.where(suppress_from_homepage: true).pluck(:id)
|
||||
}
|
||||
|
||||
result = CategoryAndTopicLists.new
|
||||
@ -245,6 +248,7 @@ class CategoriesController < ApplicationController
|
||||
:show_subcategory_list,
|
||||
:num_featured_topics,
|
||||
:default_view,
|
||||
:subcategory_list_style,
|
||||
:custom_fields => [params[:custom_fields].try(:keys)],
|
||||
:permissions => [*p.try(:keys)],
|
||||
:allowed_tags => [],
|
||||
@ -260,9 +264,10 @@ class CategoriesController < ApplicationController
|
||||
@staff_action_logger = StaffActionLogger.new(current_user)
|
||||
end
|
||||
|
||||
def include_topics
|
||||
def include_topics(parent_category=nil)
|
||||
view_context.mobile_view? ||
|
||||
params[:include_topics] ||
|
||||
(parent_category && parent_category.subcategory_list_includes_topics?) ||
|
||||
SiteSetting.desktop_category_page_style == "categories_with_featured_topics".freeze
|
||||
end
|
||||
end
|
||||
|
||||
@ -46,6 +46,7 @@ class ListController < ApplicationController
|
||||
:parent_category_category_top,
|
||||
# top pages (ie. with a period)
|
||||
TopTopic.periods.map { |p| :"top_#{p}" },
|
||||
TopTopic.periods.map { |p| :"top_#{p}_feed" },
|
||||
TopTopic.periods.map { |p| :"category_top_#{p}" },
|
||||
TopTopic.periods.map { |p| :"category_none_top_#{p}" },
|
||||
TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" },
|
||||
@ -168,7 +169,7 @@ class ListController < ApplicationController
|
||||
@link = "#{Discourse.base_url}/top"
|
||||
@atom_link = "#{Discourse.base_url}/top.rss"
|
||||
@description = I18n.t("rss_description.top")
|
||||
@topic_list = TopicQuery.new(nil).list_top_for("monthly")
|
||||
@topic_list = TopicQuery.new(nil).list_top_for(SiteSetting.top_page_default_timeframe.to_sym)
|
||||
|
||||
render 'list', formats: [:rss]
|
||||
end
|
||||
@ -232,7 +233,7 @@ class ListController < ApplicationController
|
||||
list.for_period = period
|
||||
list.more_topics_url = construct_url_with(:next, top_options)
|
||||
list.prev_topics_url = construct_url_with(:prev, top_options)
|
||||
@rss = "top"
|
||||
@rss = "top_#{period}"
|
||||
|
||||
if use_crawler_layout?
|
||||
@title = I18n.t("js.filters.top.#{period}.title")
|
||||
@ -252,6 +253,19 @@ class ListController < ApplicationController
|
||||
define_method("parent_category_category_top_#{period}") do
|
||||
self.send("top_#{period}", category: @category.id)
|
||||
end
|
||||
|
||||
# rss feed
|
||||
define_method("top_#{period}_feed") do |options = nil|
|
||||
discourse_expires_in 1.minute
|
||||
|
||||
@description = I18n.t("rss_description.top_#{period}")
|
||||
@title = "#{SiteSetting.title} - #{@description}"
|
||||
@link = "#{Discourse.base_url}/top/#{period}"
|
||||
@atom_link = "#{Discourse.base_url}/top/#{period}.rss"
|
||||
@topic_list = TopicQuery.new(nil).list_top_for(period)
|
||||
|
||||
render 'list', formats: [:rss]
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
@ -109,14 +109,15 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def cooked
|
||||
post = find_post_from_params
|
||||
render json: {cooked: post.cooked}
|
||||
render json: { cooked: find_post_from_params.cooked }
|
||||
end
|
||||
|
||||
def raw_email
|
||||
params.require(:id)
|
||||
post = Post.unscoped.find(params[:id].to_i)
|
||||
guardian.ensure_can_view_raw_email!(post)
|
||||
render json: { raw_email: post.raw_email }
|
||||
text, html = Email.extract_parts(post.raw_email)
|
||||
render json: { raw_email: post.raw_email, text_part: text, html_part: html }
|
||||
end
|
||||
|
||||
def short_link
|
||||
@ -377,17 +378,19 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def bookmark
|
||||
post = find_post_from_params
|
||||
|
||||
if params[:bookmarked] == "true"
|
||||
post = find_post_from_params
|
||||
PostAction.act(current_user, post, PostActionType.types[:bookmark])
|
||||
else
|
||||
post_action = PostAction.find_by(post_id: params[:post_id], user_id: current_user.id)
|
||||
post = post_action.post
|
||||
raise Discourse::InvalidParameters unless post_action
|
||||
|
||||
PostAction.remove_act(current_user, post, PostActionType.types[:bookmark])
|
||||
end
|
||||
|
||||
tu = TopicUser.get(post.topic, current_user)
|
||||
|
||||
render_json_dump(topic_bookmarked: tu.try(:bookmarked))
|
||||
topic_user = TopicUser.get(post.topic, current_user)
|
||||
render_json_dump(topic_bookmarked: topic_user.try(:bookmarked))
|
||||
end
|
||||
|
||||
def wiki
|
||||
|
||||
@ -3,7 +3,7 @@ require_dependency 'site_serializer'
|
||||
class SiteController < ApplicationController
|
||||
layout false
|
||||
skip_before_filter :preload_json, :check_xhr
|
||||
skip_before_filter :redirect_to_login_if_required, only: ['basic_info']
|
||||
skip_before_filter :redirect_to_login_if_required, only: ['basic_info', 'statistics']
|
||||
|
||||
def site
|
||||
render json: Site.json_for(guardian)
|
||||
@ -42,4 +42,9 @@ class SiteController < ApplicationController
|
||||
# this info is always available cause it can be scraped from a 404 page
|
||||
render json: results
|
||||
end
|
||||
|
||||
def statistics
|
||||
return redirect_to path('/') unless SiteSetting.share_anonymized_statistics?
|
||||
render json: About.fetch_cached_stats
|
||||
end
|
||||
end
|
||||
|
||||
@ -81,7 +81,8 @@ class UploadsController < ApplicationController
|
||||
if filename == "image.png" && SiteSetting.convert_pasted_images_to_hq_jpg
|
||||
jpeg_path = "#{File.dirname(tempfile.path)}/image.jpg"
|
||||
OptimizedImage.ensure_safe_paths!(tempfile.path, jpeg_path)
|
||||
`convert #{tempfile.path} -quality #{SiteSetting.convert_pasted_images_quality} #{jpeg_path}`
|
||||
|
||||
Discourse::Utils.execute_command('convert', tempfile.path, '-quality', SiteSetting.convert_pasted_images_quality.to_s, jpeg_path)
|
||||
# only change the format of the image when JPG is at least 5% smaller
|
||||
if File.size(jpeg_path) < File.size(tempfile.path) * 0.95
|
||||
filename = "image.jpg"
|
||||
|
||||
@ -550,7 +550,13 @@ class UsersController < ApplicationController
|
||||
if Guardian.new(@user).can_access_forum?
|
||||
@user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message
|
||||
log_on_user(@user)
|
||||
return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user)
|
||||
|
||||
if Wizard.user_requires_completion?(@user)
|
||||
return redirect_to(wizard_path)
|
||||
elsif destination_url = cookies[:destination_url]
|
||||
cookies[:destination_url] = nil
|
||||
return redirect_to(destination_url)
|
||||
end
|
||||
else
|
||||
@needs_approval = true
|
||||
end
|
||||
@ -581,7 +587,7 @@ class UsersController < ApplicationController
|
||||
|
||||
if @user.active
|
||||
render_json_error(I18n.t('activation.activated'), status: 409)
|
||||
else @user
|
||||
else
|
||||
@email_token = @user.email_tokens.unconfirmed.active.first
|
||||
enqueue_activation_email
|
||||
render nothing: true
|
||||
|
||||
41
app/jobs/onceoff/migrate_custom_emojis.rb
Normal file
41
app/jobs/onceoff/migrate_custom_emojis.rb
Normal file
@ -0,0 +1,41 @@
|
||||
module Jobs
|
||||
class MigrateCustomEmojis < Jobs::Onceoff
|
||||
def execute_onceoff(args)
|
||||
return if Rails.env.test?
|
||||
|
||||
Dir["#{Rails.root}/#{Emoji.base_directory}/*.{png,gif}"].each do |path|
|
||||
name = File.basename(path, File.extname(path))
|
||||
|
||||
File.open(path) do |file|
|
||||
upload = Upload.create_for(
|
||||
Discourse.system_user.id,
|
||||
file,
|
||||
File.basename(path),
|
||||
file.size,
|
||||
image_type: 'custom_emoji'
|
||||
)
|
||||
|
||||
if upload.persisted?
|
||||
custom_emoji = CustomEmoji.new(name: name, upload: upload)
|
||||
|
||||
if !custom_emoji.save
|
||||
warn("Failed to create custom emoji '#{name}': #{custom_emoji.errors.full_messages}")
|
||||
end
|
||||
else
|
||||
warn("Failed to create upload for '#{name}' custom emoji: #{upload.errors.full_messages}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Emoji.clear_cache
|
||||
|
||||
Post.where("cooked LIKE '%#{Emoji.base_url}%'").find_each do |post|
|
||||
post.rebake!
|
||||
end
|
||||
end
|
||||
|
||||
def warn(message)
|
||||
Rails.logger.warn(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -3,32 +3,24 @@ require 'excon'
|
||||
module Jobs
|
||||
class EmitWebHookEvent < Jobs::Base
|
||||
def execute(args)
|
||||
raise Discourse::InvalidParameters.new(:web_hook_id) unless args[:web_hook_id].present?
|
||||
raise Discourse::InvalidParameters.new(:event_type) unless args[:event_type].present?
|
||||
|
||||
args = args.dup
|
||||
|
||||
if args[:topic_id]
|
||||
args[:topic_view] = TopicView.new(args[:topic_id], Discourse.system_user)
|
||||
[:web_hook_id, :event_type].each do |key|
|
||||
raise Discourse::InvalidParameters.new(key) unless args[key].present?
|
||||
end
|
||||
|
||||
if args[:post_id]
|
||||
# deleted post so skip
|
||||
return unless args[:post] = Post.find_by(id: args[:post_id])
|
||||
end
|
||||
web_hook = WebHook.find_by(id: args[:web_hook_id])
|
||||
raise Discourse::InvalidParameters(:web_hook_id) if web_hook.blank?
|
||||
|
||||
if args[:user_id]
|
||||
return unless args[:user] = User.find_by(id: args[:user_id])
|
||||
end
|
||||
|
||||
web_hook = WebHook.find(args[:web_hook_id])
|
||||
|
||||
unless args[:event_type] == 'ping'
|
||||
unless ping_event?(args[:event_type])
|
||||
return unless web_hook.active?
|
||||
|
||||
return if web_hook.group_ids.present? && (args[:group_id].present? ||
|
||||
!web_hook.group_ids.include?(args[:group_id]))
|
||||
|
||||
return if web_hook.category_ids.present? && (!args[:category_id].present? ||
|
||||
!web_hook.category_ids.include?(args[:category_id]))
|
||||
|
||||
event_type = args[:event_type].to_s
|
||||
return unless self.send("setup_#{event_type}", args)
|
||||
end
|
||||
|
||||
web_hook_request(args, web_hook)
|
||||
@ -36,12 +28,56 @@ module Jobs
|
||||
|
||||
private
|
||||
|
||||
def web_hook_request(args, web_hook)
|
||||
def guardian
|
||||
Guardian.new(Discourse.system_user)
|
||||
end
|
||||
|
||||
def setup_post(args)
|
||||
post = Post.find_by(id: args[:post_id])
|
||||
return if post.blank?
|
||||
args[:payload] = WebHookPostSerializer.new(post, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
def setup_topic(args)
|
||||
topic_view = (TopicView.new(args[:topic_id], Discourse.system_user) rescue nil)
|
||||
return if topic_view.blank?
|
||||
args[:payload] = WebHookTopicViewSerializer.new(topic_view, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
def setup_user(args)
|
||||
user = User.find_by(id: args[:user_id])
|
||||
return if user.blank?
|
||||
args[:payload] = WebHookUserSerializer.new(user, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
def ping_event?(event_type)
|
||||
event_type.to_s == 'ping'.freeze
|
||||
end
|
||||
|
||||
def build_web_hook_body(args, web_hook)
|
||||
body = {}
|
||||
guardian = Guardian.new(Discourse.system_user)
|
||||
event_type = args[:event_type].to_s
|
||||
|
||||
if ping_event?(event_type)
|
||||
body[:ping] = 'OK'
|
||||
else
|
||||
body[event_type] = args[:payload]
|
||||
end
|
||||
|
||||
new_body = Plugin::Filter.apply(:after_build_web_hook_body, self, body)
|
||||
|
||||
MultiJson.dump(new_body)
|
||||
end
|
||||
|
||||
def web_hook_request(args, web_hook)
|
||||
uri = URI(web_hook.payload_url)
|
||||
conn = Excon.new(uri.to_s,
|
||||
ssl_verify_peer: web_hook.verify_certificate,
|
||||
retry_limit: 0)
|
||||
|
||||
conn = Excon.new(
|
||||
uri.to_s,
|
||||
ssl_verify_peer: web_hook.verify_certificate,
|
||||
retry_limit: 0
|
||||
)
|
||||
|
||||
body = build_web_hook_body(args, web_hook)
|
||||
web_hook_event = WebHookEvent.create!(web_hook_id: web_hook.id)
|
||||
@ -53,6 +89,7 @@ module Jobs
|
||||
else
|
||||
'application/json'
|
||||
end
|
||||
|
||||
headers = {
|
||||
'Accept' => '*/*',
|
||||
'Connection' => 'close',
|
||||
@ -64,6 +101,7 @@ module Jobs
|
||||
'X-Discourse-Event-Id' => web_hook_event.id,
|
||||
'X-Discourse-Event-Type' => args[:event_type]
|
||||
}
|
||||
|
||||
headers['X-Discourse-Event'] = args[:event_name].to_s if args[:event_name].present?
|
||||
|
||||
if web_hook.secret.present?
|
||||
@ -72,45 +110,23 @@ module Jobs
|
||||
|
||||
now = Time.zone.now
|
||||
response = conn.post(headers: headers, body: body)
|
||||
|
||||
web_hook_event.update!(
|
||||
headers: MultiJson.dump(headers),
|
||||
payload: body,
|
||||
status: response.status,
|
||||
response_headers: MultiJson.dump(response.headers),
|
||||
response_body: response.body,
|
||||
duration: ((Time.zone.now - now) * 1000).to_i
|
||||
)
|
||||
|
||||
MessageBus.publish("/web_hook_events/#{web_hook.id}", {
|
||||
web_hook_event_id: web_hook_event.id,
|
||||
event_type: args[:event_type]
|
||||
}, user_ids: User.human_users.staff.pluck(:id))
|
||||
rescue
|
||||
web_hook_event.destroy!
|
||||
end
|
||||
|
||||
web_hook_event.update_attributes!(headers: MultiJson.dump(headers),
|
||||
payload: body,
|
||||
status: response.status,
|
||||
response_headers: MultiJson.dump(response.headers),
|
||||
response_body: response.body,
|
||||
duration: ((Time.zone.now - now) * 1000).to_i)
|
||||
MessageBus.publish("/web_hook_events/#{web_hook.id}", {
|
||||
web_hook_event_id: web_hook_event.id,
|
||||
event_type: args[:event_type]
|
||||
}, user_ids: User.staff.pluck(:id))
|
||||
end
|
||||
|
||||
def build_web_hook_body(args, web_hook)
|
||||
body = {}
|
||||
guardian = Guardian.new(Discourse.system_user)
|
||||
|
||||
if topic_view = args[:topic_view]
|
||||
body[:topic] = TopicViewSerializer.new(topic_view, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
if post = args[:post]
|
||||
body[:post] = PostSerializer.new(post, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
if user = args[:user]
|
||||
body[:user] = UserSerializer.new(user, scope: guardian, root: false).as_json
|
||||
end
|
||||
|
||||
body[:ping] = 'OK' if args[:event_type] == 'ping'
|
||||
|
||||
raise Discourse::InvalidParameters.new if body.empty?
|
||||
|
||||
MultiJson.dump(body)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
8
app/jobs/regular/rebake_custom_emoji_posts.rb
Normal file
8
app/jobs/regular/rebake_custom_emoji_posts.rb
Normal file
@ -0,0 +1,8 @@
|
||||
module Jobs
|
||||
class RebakeCustomEmojiPosts < Jobs::Base
|
||||
def execute(args)
|
||||
name = args[:name]
|
||||
Post.where("raw LIKE '%:#{name}:%'").find_each { |post| post.rebake! }
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,18 +0,0 @@
|
||||
module Jobs
|
||||
|
||||
class ResizeEmoji < Jobs::Base
|
||||
|
||||
def execute(args)
|
||||
path = args[:path]
|
||||
return unless File.exists?(path)
|
||||
|
||||
opts = {
|
||||
allow_animation: true,
|
||||
force_aspect_ratio: SiteSetting.enforce_square_emoji
|
||||
}
|
||||
# make sure emoji aren't too big
|
||||
OptimizedImage.downsize(path, path, "100x100", opts)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@ -67,16 +67,13 @@ module Jobs
|
||||
signup_after_approval
|
||||
}
|
||||
|
||||
def message_for_email(user, post, type, notification,
|
||||
notification_type=nil, notification_data_hash=nil,
|
||||
email_token=nil, to_address=nil)
|
||||
|
||||
def message_for_email(user, post, type, notification, notification_type=nil, notification_data_hash=nil, email_token=nil, to_address=nil)
|
||||
set_skip_context(type, user.id, to_address || user.email, post.try(:id))
|
||||
|
||||
return skip_message(I18n.t("email_log.anonymous_user")) if user.anonymous?
|
||||
return skip_message(I18n.t("email_log.suspended_not_pm")) if user.suspended? && type.to_s != "user_private_message"
|
||||
|
||||
return if user.staged && type == :digest
|
||||
return if user.staged && type.to_s == "digest"
|
||||
|
||||
seen_recently = (user.last_seen_at.present? && user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago)
|
||||
seen_recently = false if user.user_option.email_always || user.staged
|
||||
@ -87,9 +84,7 @@ module Jobs
|
||||
return skip_message(I18n.t('email_log.seen_recently')) if seen_recently && !user.suspended?
|
||||
end
|
||||
|
||||
if post
|
||||
email_args[:post] = post
|
||||
end
|
||||
email_args[:post] = post if post
|
||||
|
||||
if notification || notification_type
|
||||
email_args[:notification_type] ||= notification_type || notification.try(:notification_type)
|
||||
@ -118,18 +113,13 @@ module Jobs
|
||||
end
|
||||
|
||||
skip_reason = skip_email_for_post(post, user)
|
||||
return skip_message(skip_reason) if skip_reason
|
||||
return skip_message(skip_reason) if skip_reason.present?
|
||||
|
||||
# Make sure that mailer exists
|
||||
raise Discourse::InvalidParameters.new("type=#{type}") unless UserNotifications.respond_to?(type)
|
||||
|
||||
if email_token.present?
|
||||
email_args[:email_token] = email_token
|
||||
end
|
||||
|
||||
if type.to_s == "notify_old_email"
|
||||
email_args[:new_email] = user.email
|
||||
end
|
||||
email_args[:email_token] = email_token if email_token.present?
|
||||
email_args[:new_email] = user.email if type.to_s == "notify_old_email"
|
||||
|
||||
if EmailLog.reached_max_emails?(user)
|
||||
return skip_message(I18n.t('email_log.exceeded_emails_limit'))
|
||||
@ -144,9 +134,7 @@ module Jobs
|
||||
end
|
||||
|
||||
# Update the to address if we have a custom one
|
||||
if message && to_address.present?
|
||||
message.to = to_address
|
||||
end
|
||||
message.to = to_address if message && to_address.present?
|
||||
|
||||
[message, nil]
|
||||
end
|
||||
@ -179,12 +167,10 @@ module Jobs
|
||||
return I18n.t('email_log.topic_nil') if post.topic.blank?
|
||||
return I18n.t('email_log.post_user_deleted') if post.user.blank?
|
||||
return I18n.t('email_log.post_deleted') if post.user_deleted?
|
||||
return I18n.t('email_log.user_suspended') if (user.suspended? && !post.user.try(:staff?))
|
||||
return I18n.t('email_log.user_suspended') if user.suspended? && !post.user&.staff?
|
||||
|
||||
if !user.user_option.email_always? &&
|
||||
PostTiming.where(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id).present?
|
||||
return I18n.t('email_log.already_read')
|
||||
end
|
||||
already_read = !user.user_option.email_always? && PostTiming.exists?(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id)
|
||||
return I18n.t('email_log.already_read') if already_read
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
@ -22,11 +22,13 @@ module Jobs
|
||||
.joins("LEFT JOIN user_avatars ua ON (ua.gravatar_upload_id = uploads.id OR ua.custom_upload_id = uploads.id)")
|
||||
.joins("LEFT JOIN user_profiles up ON up.profile_background = uploads.url OR up.card_background = uploads.url")
|
||||
.joins("LEFT JOIN categories c ON c.uploaded_logo_id = uploads.id OR c.uploaded_background_id = uploads.id")
|
||||
.joins("LEFT JOIN custom_emojis ce ON ce.upload_id = uploads.id")
|
||||
.where("pu.upload_id IS NULL")
|
||||
.where("u.uploaded_avatar_id IS NULL")
|
||||
.where("ua.gravatar_upload_id IS NULL AND ua.custom_upload_id IS NULL")
|
||||
.where("up.profile_background IS NULL AND up.card_background IS NULL")
|
||||
.where("c.uploaded_logo_id IS NULL AND c.uploaded_background_id IS NULL")
|
||||
.where("ce.upload_id IS NULL")
|
||||
.where("uploads.url NOT IN (?)", ignore_urls)
|
||||
|
||||
result.find_each do |upload|
|
||||
|
||||
@ -15,18 +15,18 @@ module Jobs
|
||||
def target_user_ids
|
||||
# Users who want to receive digest email within their chosen digest email frequency
|
||||
query = User.real
|
||||
.where(active: true, staged: false)
|
||||
.joins(:user_option)
|
||||
.not_suspended
|
||||
.where(user_options: {email_digests: true})
|
||||
.activated
|
||||
.where(staged: false)
|
||||
.joins(:user_option, :user_stat)
|
||||
.where("user_options.email_digests")
|
||||
.where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}")
|
||||
.where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
||||
.where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
||||
.where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})")
|
||||
|
||||
# If the site requires approval, make sure the user is approved
|
||||
if SiteSetting.must_approve_users?
|
||||
query = query.where("approved OR moderator OR admin")
|
||||
end
|
||||
query = query.where("approved OR moderator OR admin") if SiteSetting.must_approve_users?
|
||||
|
||||
query.pluck(:id)
|
||||
end
|
||||
|
||||
@ -357,6 +357,12 @@ class UserNotifications < ActionMailer::Base
|
||||
user = opts[:user]
|
||||
locale = user_locale(user)
|
||||
|
||||
template = "user_notifications.user_#{notification_type}"
|
||||
if post.topic.private_message?
|
||||
template << "_pm"
|
||||
template << "_staged" if user.staged?
|
||||
end
|
||||
|
||||
# category name
|
||||
category = Topic.find_by(id: post.topic_id).category
|
||||
if opts[:show_category_in_subject] && post.topic_id && category && !category.uncategorized?
|
||||
@ -384,10 +390,7 @@ class UserNotifications < ActionMailer::Base
|
||||
end
|
||||
end
|
||||
|
||||
reached_limit = SiteSetting.max_emails_per_day_per_user > 0
|
||||
reached_limit &&= (EmailLog.where(user_id: user.id, skipped: false)
|
||||
.where('created_at > ?', 1.day.ago)
|
||||
.count) >= (SiteSetting.max_emails_per_day_per_user-1)
|
||||
translation_override_exists = TranslationOverride.where(locale: SiteSetting.default_locale, translation_key: "#{template}.text_body_template").exists?
|
||||
|
||||
if opts[:use_invite_template]
|
||||
if post.topic.private_message?
|
||||
@ -397,36 +400,42 @@ class UserNotifications < ActionMailer::Base
|
||||
end
|
||||
topic_excerpt = post.excerpt.tr("\n", " ") if post.is_first_post? && post.excerpt
|
||||
message = I18n.t(invite_template, username: username, topic_title: title, topic_excerpt: topic_excerpt, site_title: SiteSetting.title, site_description: SiteSetting.site_description)
|
||||
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/invite',
|
||||
format: :html,
|
||||
locals: { message: PrettyText.cook(message, sanitize: false).html_safe,
|
||||
classes: RTL.new(user).css_class
|
||||
}
|
||||
)
|
||||
else
|
||||
in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to
|
||||
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/notification',
|
||||
format: :html,
|
||||
locals: { context_posts: context_posts,
|
||||
reached_limit: reached_limit,
|
||||
post: post,
|
||||
in_reply_to_post: in_reply_to_post,
|
||||
classes: RTL.new(user).css_class
|
||||
}
|
||||
)
|
||||
message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "");
|
||||
end
|
||||
|
||||
template = "user_notifications.user_#{notification_type}"
|
||||
if post.topic.private_message?
|
||||
template << "_pm"
|
||||
template << "_staged" if user.staged?
|
||||
unless translation_override_exists
|
||||
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/invite',
|
||||
format: :html,
|
||||
locals: { message: PrettyText.cook(message, sanitize: false).html_safe,
|
||||
classes: RTL.new(user).css_class
|
||||
}
|
||||
)
|
||||
end
|
||||
else
|
||||
reached_limit = SiteSetting.max_emails_per_day_per_user > 0
|
||||
reached_limit &&= (EmailLog.where(user_id: user.id, skipped: false)
|
||||
.where('created_at > ?', 1.day.ago)
|
||||
.count) >= (SiteSetting.max_emails_per_day_per_user-1)
|
||||
|
||||
in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to
|
||||
message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "");
|
||||
|
||||
unless translation_override_exists
|
||||
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/notification',
|
||||
format: :html,
|
||||
locals: { context_posts: context_posts,
|
||||
reached_limit: reached_limit,
|
||||
post: post,
|
||||
in_reply_to_post: in_reply_to_post,
|
||||
classes: RTL.new(user).css_class
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
email_opts = {
|
||||
topic_title: title,
|
||||
topic_title_url_encoded: title ? URI.encode(title) : title,
|
||||
message: message,
|
||||
url: post.url,
|
||||
post_id: post.id,
|
||||
@ -444,13 +453,17 @@ class UserNotifications < ActionMailer::Base
|
||||
private_reply: post.topic.private_message?,
|
||||
include_respond_instructions: !(user.suspended? || user.staged?),
|
||||
template: template,
|
||||
html_override: html,
|
||||
site_description: SiteSetting.site_description,
|
||||
site_title: SiteSetting.title,
|
||||
site_title_url_encoded: URI.encode(SiteSetting.title),
|
||||
style: :notification,
|
||||
locale: locale
|
||||
}
|
||||
|
||||
unless translation_override_exists
|
||||
email_opts[:html_override] = html
|
||||
end
|
||||
|
||||
# If we have a display name, change the from address
|
||||
email_opts[:from_alias] = from_alias if from_alias.present?
|
||||
|
||||
|
||||
@ -35,14 +35,12 @@ class About
|
||||
|
||||
def moderators
|
||||
@moderators ||= User.where(moderator: true, admin: false)
|
||||
.where.not(id: Discourse::SYSTEM_USER_ID)
|
||||
.human_users
|
||||
.order(:username_lower)
|
||||
end
|
||||
|
||||
def admins
|
||||
@admins ||= User.where(admin: true)
|
||||
.where.not(id: Discourse::SYSTEM_USER_ID)
|
||||
.order(:username_lower)
|
||||
@admins ||= User.where(admin: true).human_users.order(:username_lower)
|
||||
end
|
||||
|
||||
def stats
|
||||
|
||||
@ -507,6 +507,10 @@ SQL
|
||||
self.where(slug: category_slug, parent_category_id: nil).first
|
||||
end
|
||||
end
|
||||
|
||||
def subcategory_list_includes_topics?
|
||||
subcategory_list_style.end_with?("with_featured_topics")
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
6
app/models/custom_emoji.rb
Normal file
6
app/models/custom_emoji.rb
Normal file
@ -0,0 +1,6 @@
|
||||
class CustomEmoji < ActiveRecord::Base
|
||||
belongs_to :upload
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :upload_id, presence: true
|
||||
end
|
||||
@ -40,7 +40,7 @@ class EmbeddableHost < ActiveRecord::Base
|
||||
|
||||
def host_must_be_valid
|
||||
if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,7}(:[0-9]{1,5})?(\/.*)?\Z/i &&
|
||||
host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/ &&
|
||||
host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ &&
|
||||
host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i
|
||||
errors.add(:host, I18n.t('errors.messages.invalid'))
|
||||
end
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
class Emoji
|
||||
# update this to clear the cache
|
||||
EMOJI_VERSION = "v2"
|
||||
EMOJI_VERSION = "v3"
|
||||
|
||||
include ActiveModel::SerializerSupport
|
||||
|
||||
@ -14,14 +14,6 @@ class Emoji
|
||||
@path = path
|
||||
end
|
||||
|
||||
def remove
|
||||
return if path.blank?
|
||||
if File.exists?(path)
|
||||
File.delete(path) rescue nil
|
||||
Emoji.clear_cache
|
||||
end
|
||||
end
|
||||
|
||||
def self.all
|
||||
Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
|
||||
end
|
||||
@ -46,14 +38,6 @@ class Emoji
|
||||
Emoji.custom.detect { |e| e.name == name }
|
||||
end
|
||||
|
||||
def self.create_from_path(path)
|
||||
extension = File.extname(path)
|
||||
Emoji.new(path).tap do |e|
|
||||
e.name = File.basename(path, ".*")
|
||||
e.url = "#{base_url}/#{e.name}#{extension}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_from_db_item(emoji)
|
||||
name = emoji["name"]
|
||||
filename = "#{emoji['filename'] || name}.png"
|
||||
@ -63,22 +47,6 @@ class Emoji
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_for(file, name)
|
||||
extension = File.extname(file.original_filename)
|
||||
path = "#{Emoji.base_directory}/#{name}#{extension}"
|
||||
full_path = "#{Rails.root}/#{path}"
|
||||
|
||||
# store the emoji
|
||||
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
||||
File.open(path, "wb") { |f| f << file.tempfile.read }
|
||||
# clear the cache
|
||||
Emoji.clear_cache
|
||||
# launch resize job
|
||||
Jobs.enqueue(:resize_emoji, path: full_path)
|
||||
# return created emoji
|
||||
Emoji[name]
|
||||
end
|
||||
|
||||
def self.cache_key(name)
|
||||
"#{name}:#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
|
||||
end
|
||||
@ -124,9 +92,12 @@ class Emoji
|
||||
def self.load_custom
|
||||
result = []
|
||||
|
||||
Dir.glob(File.join(Emoji.base_directory, "*.{png,gif}"))
|
||||
.sort
|
||||
.each { |emoji| result << Emoji.create_from_path(emoji) }
|
||||
CustomEmoji.all.each do |emoji|
|
||||
result << Emoji.new.tap do |e|
|
||||
e.name = emoji.name
|
||||
e.url = emoji.upload.url
|
||||
end
|
||||
end
|
||||
|
||||
Plugin::CustomEmoji.emojis.each do |name, url|
|
||||
result << Emoji.new.tap do |e|
|
||||
|
||||
@ -11,6 +11,8 @@ class GlobalSetting
|
||||
# for legacy reasons
|
||||
REDIS_SECRET_KEY = 'SECRET_TOKEN'
|
||||
|
||||
REDIS_VALIDATE_SECONDS = 30
|
||||
|
||||
# In Rails secret_key_base is used to encrypt the cookie store
|
||||
# the cookie store contains session data
|
||||
# Discourse also uses this secret key to digest user auth tokens
|
||||
@ -19,9 +21,22 @@ class GlobalSetting
|
||||
# - generate a token on the fly if needed and cache in redis
|
||||
# - enforce rules about token format falling back to redis if needed
|
||||
def self.safe_secret_key_base
|
||||
|
||||
if @safe_secret_key_base && @token_in_redis && (@token_last_validated + REDIS_VALIDATE_SECONDS) < Time.now
|
||||
@token_last_validated = Time.now
|
||||
token = $redis.without_namespace.get(REDIS_SECRET_KEY)
|
||||
if token.nil?
|
||||
$redis.without_namespace.set(REDIS_SECRET_KEY, @safe_secret_key_base)
|
||||
end
|
||||
end
|
||||
|
||||
@safe_secret_key_base ||= begin
|
||||
token = secret_key_base
|
||||
if token.blank? || token !~ VALID_SECRET_KEY
|
||||
|
||||
@token_in_redis = true
|
||||
@token_last_validated = Time.now
|
||||
|
||||
token = $redis.without_namespace.get(REDIS_SECRET_KEY)
|
||||
unless token && token =~ VALID_SECRET_KEY
|
||||
token = SecureRandom.hex(64)
|
||||
@ -39,8 +54,21 @@ class GlobalSetting
|
||||
default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__))
|
||||
default_provider.keys.concat(@provider.keys).uniq.each do |key|
|
||||
default = default_provider.lookup(key, nil)
|
||||
|
||||
instance_variable_set("@#{key}_cache", nil)
|
||||
|
||||
define_singleton_method(key) do
|
||||
provider.lookup(key, default)
|
||||
val = instance_variable_get("@#{key}_cache")
|
||||
unless val.nil?
|
||||
val == :missing ? nil : val
|
||||
else
|
||||
val = provider.lookup(key, default)
|
||||
if val.nil?
|
||||
val = :missing
|
||||
end
|
||||
instance_variable_set("@#{key}_cache", val)
|
||||
val == :missing ? nil : val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -62,6 +62,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do
|
||||
send_welcome_message
|
||||
notify_invitee
|
||||
send_password_instructions
|
||||
enqueue_activation_mail
|
||||
delete_duplicate_invites
|
||||
end
|
||||
|
||||
@ -126,6 +127,13 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_activation_mail
|
||||
if invited_user.has_password?
|
||||
email_token = invited_user.email_tokens.create(email: invited_user.email)
|
||||
Jobs.enqueue(:critical_user_email, type: :signup, user_id: invited_user.id, email_token: email_token.token)
|
||||
end
|
||||
end
|
||||
|
||||
def notify_invitee
|
||||
if inviter = invite.invited_by
|
||||
inviter.notifications.create(notification_type: Notification.types[:invitee_accepted],
|
||||
|
||||
@ -12,10 +12,11 @@ class Notification < ActiveRecord::Base
|
||||
scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id')
|
||||
.where('topics.id IS NULL OR topics.deleted_at IS NULL') }
|
||||
|
||||
after_save :refresh_notification_count
|
||||
after_destroy :refresh_notification_count
|
||||
|
||||
after_commit :send_email
|
||||
# This is super weird because the tests fail if we don't specify `on: :destroy`
|
||||
# TODO: Revert back to default in Rails 5
|
||||
after_commit :refresh_notification_count, on: :destroy
|
||||
after_commit :refresh_notification_count, on: [:create, :update]
|
||||
|
||||
def self.ensure_consistency!
|
||||
Notification.exec_sql("
|
||||
|
||||
@ -215,8 +215,11 @@ class OptimizedImage < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.convert_with(instructions, to)
|
||||
`#{instructions.join(" ")} &> /dev/null`
|
||||
return false if $?.exitstatus != 0
|
||||
begin
|
||||
Discourse::Utils.execute_command(*instructions)
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
|
||||
ImageOptim.new.optimize_image!(to)
|
||||
true
|
||||
|
||||
@ -833,7 +833,7 @@ SQL
|
||||
.update_all(dismissed_banner_key: nil)
|
||||
|
||||
self.archetype = Archetype.banner
|
||||
self.add_moderator_post(user, I18n.t("archetypes.banner.message.make"))
|
||||
self.add_small_action(user, "banner.enabled")
|
||||
self.save
|
||||
|
||||
MessageBus.publish('/site/banner', banner)
|
||||
@ -841,7 +841,7 @@ SQL
|
||||
|
||||
def remove_banner!(user)
|
||||
self.archetype = Archetype.default
|
||||
self.add_moderator_post(user, I18n.t("archetypes.banner.message.remove"))
|
||||
self.add_small_action(user, "banner.disabled")
|
||||
self.save
|
||||
|
||||
MessageBus.publish('/site/banner', nil)
|
||||
|
||||
@ -57,7 +57,12 @@ class Upload < ActiveRecord::Base
|
||||
end
|
||||
|
||||
# list of image types that will be cropped
|
||||
CROPPED_IMAGE_TYPES ||= %w{avatar profile_background card_background}
|
||||
CROPPED_IMAGE_TYPES ||= %w{
|
||||
avatar
|
||||
profile_background
|
||||
card_background
|
||||
custom_emoji
|
||||
}
|
||||
|
||||
WHITELISTED_SVG_ELEMENTS ||= %w{
|
||||
circle
|
||||
@ -92,7 +97,7 @@ class Upload < ActiveRecord::Base
|
||||
# options
|
||||
# - content_type
|
||||
# - origin (url)
|
||||
# - image_type ("avatar", "profile_background", "card_background")
|
||||
# - image_type ("avatar", "profile_background", "card_background", "custom_emoji")
|
||||
# - is_attachment_for_group_message (boolean)
|
||||
def self.create_for(user_id, file, filename, filesize, options = {})
|
||||
upload = Upload.new
|
||||
@ -145,6 +150,8 @@ class Upload < ActiveRecord::Base
|
||||
max_width = 590 * max_pixel_ratio
|
||||
width, height = ImageSizer.resize(w, h, max_width: max_width, max_height: max_width)
|
||||
OptimizedImage.downsize(file.path, file.path, "#{width}x#{height}", filename: filename, allow_animation: allow_animation)
|
||||
when "custom_emoji"
|
||||
OptimizedImage.downsize(file.path, file.path, "100x100", filename: filename, allow_animation: allow_animation)
|
||||
end
|
||||
end
|
||||
|
||||
@ -246,7 +253,7 @@ class Upload < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.fix_image_orientation(path)
|
||||
`convert #{path} -auto-orient #{path}`
|
||||
Discourse::Utils.execute_command('convert', path, '-auto-orient', path)
|
||||
end
|
||||
|
||||
def self.migrate_to_new_scheme(limit=nil)
|
||||
|
||||
@ -93,7 +93,6 @@ class User < ActiveRecord::Base
|
||||
after_create :create_user_profile
|
||||
after_create :ensure_in_trust_level_group
|
||||
after_create :set_default_categories_preferences
|
||||
after_create :trigger_user_created_event
|
||||
|
||||
before_save :update_username_lower
|
||||
before_save :ensure_password_is_hashed
|
||||
@ -105,6 +104,7 @@ class User < ActiveRecord::Base
|
||||
after_save :badge_grant
|
||||
after_save :expire_old_email_tokens
|
||||
after_save :index_search
|
||||
after_commit :trigger_user_created_event, on: :create
|
||||
|
||||
before_destroy do
|
||||
# These tables don't have primary keys, so destroying them with activerecord is tricky:
|
||||
@ -124,8 +124,10 @@ class User < ActiveRecord::Base
|
||||
# set to true to optimize creation and save for imports
|
||||
attr_accessor :import_mode
|
||||
|
||||
scope :human_users, -> { where('users.id > 0') }
|
||||
|
||||
# excluding fake users like the system user or anonymous users
|
||||
scope :real, -> { where('users.id > 0').where('NOT EXISTS(
|
||||
scope :real, -> { human_users.where('NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM user_custom_fields ucf
|
||||
WHERE
|
||||
@ -1062,11 +1064,6 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def trigger_user_created_event
|
||||
DiscourseEvent.trigger(:user_created, self)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def previous_visit_at_update_required?(timestamp)
|
||||
@ -1080,6 +1077,11 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def trigger_user_created_event
|
||||
DiscourseEvent.trigger(:user_created, self)
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
@ -41,11 +41,11 @@ class WebHook < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.enqueue_topic_hooks(event, topic, user=nil)
|
||||
WebHook.enqueue_hooks(:topic, topic_id: topic.id, user_id: user&.id, category_id: topic&.category_id, event_name: event.to_s)
|
||||
WebHook.enqueue_hooks(:topic, topic_id: topic.id, category_id: topic&.category_id, event_name: event.to_s)
|
||||
end
|
||||
|
||||
def self.enqueue_post_hooks(event, post, user=nil)
|
||||
WebHook.enqueue_hooks(:post, post_id: post.id, topic_id: post&.topic_id, user_id: user&.id, category_id: post&.topic&.category_id, event_name: event.to_s)
|
||||
WebHook.enqueue_hooks(:post, post_id: post.id, category_id: post&.topic&.category_id, event_name: event.to_s)
|
||||
end
|
||||
|
||||
%i(topic_destroyed topic_recovered).each do |event|
|
||||
@ -61,6 +61,7 @@ class WebHook < ActiveRecord::Base
|
||||
%i(post_created
|
||||
post_destroyed
|
||||
post_recovered).each do |event|
|
||||
|
||||
DiscourseEvent.on(event) do |post, _, user|
|
||||
WebHook.enqueue_post_hooks(event, post, user)
|
||||
end
|
||||
|
||||
@ -91,4 +91,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
scope.can_view_action_logs?(object)
|
||||
end
|
||||
|
||||
def post_count
|
||||
object.posts.count
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -22,7 +22,8 @@ class BasicCategorySerializer < ApplicationSerializer
|
||||
:sort_ascending,
|
||||
:show_subcategory_list,
|
||||
:num_featured_topics,
|
||||
:default_view
|
||||
:default_view,
|
||||
:subcategory_list_style
|
||||
|
||||
has_one :uploaded_logo, embed: :object, serializer: CategoryUploadSerializer
|
||||
has_one :uploaded_background, embed: :object, serializer: CategoryUploadSerializer
|
||||
|
||||
17
app/serializers/web_hook_post_serializer.rb
Normal file
17
app/serializers/web_hook_post_serializer.rb
Normal file
@ -0,0 +1,17 @@
|
||||
class WebHookPostSerializer < PostSerializer
|
||||
def include_can_edit?
|
||||
false
|
||||
end
|
||||
|
||||
def can_delete
|
||||
false
|
||||
end
|
||||
|
||||
def can_recover
|
||||
false
|
||||
end
|
||||
|
||||
def can_wiki
|
||||
false
|
||||
end
|
||||
end
|
||||
11
app/serializers/web_hook_topic_view_serializer.rb
Normal file
11
app/serializers/web_hook_topic_view_serializer.rb
Normal file
@ -0,0 +1,11 @@
|
||||
require_dependency 'pinned_check'
|
||||
|
||||
class WebHookTopicViewSerializer < TopicViewSerializer
|
||||
def include_post_stream?
|
||||
false
|
||||
end
|
||||
|
||||
def include_timeline_lookup?
|
||||
false
|
||||
end
|
||||
end
|
||||
5
app/serializers/web_hook_user_serializer.rb
Normal file
5
app/serializers/web_hook_user_serializer.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class WebHookUserSerializer < UserSerializer
|
||||
# remove staff attributes
|
||||
def staff_attributes(*attrs)
|
||||
end
|
||||
end
|
||||
@ -20,7 +20,7 @@ class UserBlocker
|
||||
message_type = @opts[:message] || :blocked_by_staff
|
||||
post = SystemMessage.create(@user, message_type)
|
||||
if post && @by_user
|
||||
StaffActionLogger.new(@by_user).log_block_user(@user, {context: "#{message_type}: '#{post.topic&.title rescue ''}'"})
|
||||
StaffActionLogger.new(@by_user).log_block_user(@user, {context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}"})
|
||||
end
|
||||
end
|
||||
else
|
||||
|
||||
@ -15,8 +15,8 @@ class UserDestroyer
|
||||
# Returns a frozen instance of the User if the delete succeeded.
|
||||
def destroy(user, opts={})
|
||||
raise Discourse::InvalidParameters.new('user is nil') unless user and user.is_a?(User)
|
||||
@guardian.ensure_can_delete_user!(user)
|
||||
raise PostsExistError if !opts[:delete_posts] && user.posts.count != 0
|
||||
@guardian.ensure_can_delete_user!(user)
|
||||
|
||||
User.transaction do
|
||||
|
||||
|
||||
@ -96,15 +96,16 @@ class UserUpdater
|
||||
user.custom_fields = user.custom_fields.merge(fields)
|
||||
end
|
||||
|
||||
saved = nil
|
||||
|
||||
User.transaction do
|
||||
if attributes.key?(:muted_usernames)
|
||||
update_muted_users(attributes[:muted_usernames])
|
||||
end
|
||||
|
||||
saved = (!save_options || user.user_option.save) && user_profile.save && user.save
|
||||
if saved
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
|
||||
if saved
|
||||
# log name changes
|
||||
if attributes[:name].present? && old_user_name.downcase != attributes.fetch(:name).downcase
|
||||
StaffActionLogger.new(@actor).log_name_change(user.id, old_user_name, attributes.fetch(:name))
|
||||
@ -112,8 +113,10 @@ class UserUpdater
|
||||
StaffActionLogger.new(@actor).log_name_change(user.id, old_user_name, "")
|
||||
end
|
||||
end
|
||||
saved
|
||||
end
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user) if saved
|
||||
saved
|
||||
end
|
||||
|
||||
def update_muted_users(usernames)
|
||||
|
||||
@ -185,6 +185,8 @@ module Discourse
|
||||
require_dependency 'group'
|
||||
require_dependency 'user_field'
|
||||
require_dependency 'post_action_type'
|
||||
# Ensure that Discourse event triggers for web hooks are loaded
|
||||
require_dependency 'web_hook'
|
||||
|
||||
# So open id logs somewhere sane
|
||||
OpenID::Util.logger = Rails.logger
|
||||
|
||||
@ -12,4 +12,3 @@ end
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
self.include_root_in_json = false
|
||||
end
|
||||
|
||||
|
||||
@ -9,8 +9,8 @@ ar:
|
||||
js:
|
||||
number:
|
||||
format:
|
||||
separator: "."
|
||||
delimiter: ","
|
||||
separator: "٫"
|
||||
delimiter: "٬"
|
||||
human:
|
||||
storage_units:
|
||||
format: '%n% u'
|
||||
@ -187,28 +187,28 @@ ar:
|
||||
split_topic: "قسم هذا الموضوع في %{when}"
|
||||
invited_user: "دعى %{who} %{when}"
|
||||
invited_group: "دعى %{who} %{when}"
|
||||
removed_user: "أزال %{who} في %{when}"
|
||||
removed_group: "أزال %{who} في %{when}"
|
||||
removed_user: "أزال %{who} %{when}"
|
||||
removed_group: "أزال %{who} %{when}"
|
||||
autoclosed:
|
||||
enabled: 'أُغلق في %{when}'
|
||||
disabled: 'فُتح في %{when}'
|
||||
enabled: 'أُغلق %{when}'
|
||||
disabled: 'فُتح %{when}'
|
||||
closed:
|
||||
enabled: 'أغلقه في %{when}'
|
||||
disabled: 'فتحه في %{when}'
|
||||
enabled: 'أغلقه %{when}'
|
||||
disabled: 'فتحه %{when}'
|
||||
archived:
|
||||
enabled: 'أرشفه في %{when}'
|
||||
disabled: 'أزال أرشفته في %{when}'
|
||||
enabled: 'أرشفه %{when}'
|
||||
disabled: 'أزال أرشفته %{when}'
|
||||
pinned:
|
||||
enabled: 'ثبّته في %{when}'
|
||||
disabled: 'أزال تثبيته في %{when}'
|
||||
enabled: 'ثبّته %{when}'
|
||||
disabled: 'أزال تثبيته %{when}'
|
||||
pinned_globally:
|
||||
enabled: 'ثبّته عموميا في %{when}'
|
||||
disabled: 'أزال تثبيته في %{when}'
|
||||
enabled: 'ثبّته عموميا %{when}'
|
||||
disabled: 'أزال تثبيته %{when}'
|
||||
visible:
|
||||
enabled: 'أدرجه في %{when}'
|
||||
disabled: 'أزال إدراجه في %{when}'
|
||||
enabled: 'أدرجه %{when}'
|
||||
disabled: 'أزال إدراجه %{when}'
|
||||
topic_admin_menu: "عمليات المدير"
|
||||
wizard_required: "حان وقت اعداد منتداك! <a href='%{url}' data-auto-route='true'>شغل معالج الإعداد</a>!"
|
||||
wizard_required: "مرحبًا في دسكورس الجديد! فلنبدأ مستخدمين <a href='%{url}' data-auto-route='true'>مرشد الإعداد</a> ✨"
|
||||
emails_are_disabled: "لقد عطّل أحد المدراء الرّسائل الصادرة للجميع. لن تُرسل إشعارات عبر البريد الإلكتروني أيا كان نوعها."
|
||||
bootstrap_mode_enabled: "الموقع اﻷن مفعل بالطريقة التمهيدية لكى تتمكن من اطلاق موقعك الجديد بسهولة. سيسجل كل الأعضاء الجدد بمستوى ثقة 1 وسيكون اختيار ارسال الملخص اليومى عن طريق البريد الالكترونى مفعل. سيتم الغاء تفعيل الطريقة التمهيدية تلقائيا عندما يتخطى عدد اﻷعضاء %{min_users} عضو."
|
||||
bootstrap_mode_disabled: "سيتم الغاء تفعيل الطريقة التمهيدية خلال ال 24 ساعة القادمة"
|
||||
@ -236,7 +236,7 @@ ar:
|
||||
sign_up: "سجّل حسابا"
|
||||
log_in: "تسجيل الدخول "
|
||||
age: "العمر"
|
||||
joined: "انضم"
|
||||
joined: "انضم في"
|
||||
admin_title: "المدير"
|
||||
flags_title: "بلاغات"
|
||||
show_more: "أظهر المزيد"
|
||||
@ -334,7 +334,7 @@ ar:
|
||||
other: "{{count}} موضوع جديد."
|
||||
click_to_show: "انقر للعرض."
|
||||
preview: "معاينة"
|
||||
cancel: "ألغ"
|
||||
cancel: "إلغاء"
|
||||
save: "احفظ التعديلات"
|
||||
saving: "يحفظ..."
|
||||
saved: "حُفظت!"
|
||||
@ -365,7 +365,7 @@ ar:
|
||||
title: "تحتاج موافقة"
|
||||
none: "لا مشاركات لمراجعتها."
|
||||
edit: "حرر"
|
||||
cancel: "ألغِ"
|
||||
cancel: "إلغاء"
|
||||
view_pending: "اعرض المشاركات المعلّقة"
|
||||
has_pending_posts:
|
||||
zero: "ليس في هذا الموضوع <b>أيّة</b> مشاركات تحتاج مراجعة"
|
||||
@ -560,11 +560,11 @@ ar:
|
||||
toggle_ordering: "تبديل التحكم في الترتيب"
|
||||
subcategories: "أقسام فرعية"
|
||||
topic_sentence:
|
||||
zero: "%{count} موضوع"
|
||||
one: "موضوع"
|
||||
zero: "لا مواضيع"
|
||||
one: "موضوع واحد"
|
||||
two: "موضوعان"
|
||||
few: "%{count} مواضيع"
|
||||
many: "%{count} موضوع"
|
||||
many: "%{count} موضوعًا"
|
||||
other: "%{count} موضوع"
|
||||
topic_stat_sentence:
|
||||
zero: "لا مواضيع جديدة في ال%{unit} الماضي."
|
||||
@ -597,7 +597,7 @@ ar:
|
||||
edit: "تعديل التفضيلات"
|
||||
download_archive:
|
||||
button_text: "تحميل مواضيعي"
|
||||
confirm: "هل أنت متأكد من رغبتك في تحميل جميع مشاركاتك ؟"
|
||||
confirm: "أمتأكّد من تنزيل مشاركاتك؟"
|
||||
success: "بدأ التحميل, سيتم إعلامك برسالة عند اكتمال العملية."
|
||||
rate_limit_error: "المشاركات يمكن تحميلها لمرة واحدة في اليوم , الرجاء المحاولة غدا."
|
||||
new_private_message: "رسالة جديدة"
|
||||
@ -614,18 +614,18 @@ ar:
|
||||
statistics: "احصائيات"
|
||||
desktop_notifications:
|
||||
label: "تنبيهات سطح المكتب"
|
||||
not_supported: "عذراً , التنبيهات غير مدعومة على هذا المتصفح"
|
||||
perm_default: "تفعيل التنبيهات"
|
||||
not_supported: "نأسف، لا يدعم المتصفّح الإشعارات."
|
||||
perm_default: "فعّل الإشعارات"
|
||||
perm_denied_btn: "الوصول مرفوض"
|
||||
perm_denied_expl: "لقد رفضت صلاحية عرض الإشعارات. اسمح بظهور الإشعارات وذلك من إعدادات المتصفح."
|
||||
disable: "إيقاف التنبيهات "
|
||||
enable: "تفعيل التنبيهات"
|
||||
disable: "عطّل الإشعارات"
|
||||
enable: "فعّل الإشعارات"
|
||||
each_browser_note: "ملاحظة: عليك تغيير هذا الإعداد في كل متصفح تستخدمه."
|
||||
dismiss_notifications: "تجاهل الكل"
|
||||
dismiss_notifications_tooltip: "جعل جميع التنبيهات الغيرة مقروءة الى مقروءة. "
|
||||
first_notification: "إشعارك الأول! قم بالضغط عليه للبدء."
|
||||
disable_jump_reply: "لاتذهب إلى مشاركتي بعد الرد"
|
||||
dynamic_favicon: "إعرض عدد المواضيع الجديدة والمحدثة في أيقونة المتصفح"
|
||||
disable_jump_reply: "لا تنتقل إلى مشاركتي بعدما أردّ"
|
||||
dynamic_favicon: "أظهر عدد المواضيع الجديدة/المحدّثة في أيقونة المتصفّح"
|
||||
external_links_in_new_tab: "فتح الروابط الخارجية في ألسنة جديدة"
|
||||
enable_quoting: "فعل خاصية إقتباس النصوص المظللة"
|
||||
change: "تغيير"
|
||||
@ -636,7 +636,7 @@ ar:
|
||||
blocked_tooltip: "هذا المستخدم محظور"
|
||||
suspended_notice: "هذا المستخدم موقوف حتى تاريخ {{date}}"
|
||||
suspended_reason: "السبب:"
|
||||
github_profile: "Github"
|
||||
github_profile: "غِتهَب"
|
||||
email_activity_summary: "ملخص النشاط"
|
||||
mailing_list_mode:
|
||||
label: "وضع القائمة البريدية"
|
||||
@ -646,7 +646,7 @@ ar:
|
||||
هذه الرسائل لا تشمل المواضيع والفئات المكتومة.
|
||||
daily: "أرسل تحديثات يومية"
|
||||
individual: "أرسل لي بريدا لكل مشاركة جديدة"
|
||||
individual_no_echo: "ارسل بريد الكتروني لكل مشاركة جديدة ما عدا مواضيعي"
|
||||
individual_no_echo: "أرسل بريدًا لكلّ مشاركة جديدة عدا مشاركاتي"
|
||||
many_per_day: "أرسل لي بريدا لكل مشاركة جديدة (تقريبا {{dailyEmailEstimate}} يوميا)"
|
||||
few_per_day: "أرسل لي بريدا لكل مشاركة جديدة (تقريبا إثنتان يوميا)"
|
||||
tag_settings: "الوسوم"
|
||||
@ -667,7 +667,7 @@ ar:
|
||||
muted_categories: "المكتومة"
|
||||
muted_categories_instructions: "لن يتم إشعارك بأي جديد عن المواضيع الجديدة في هذه التصنيفات، ولن تظهر مواضيع هذه التصنيفات في قائمة المواضيع المنشورة مؤخراً."
|
||||
delete_account: "حذف الحساب"
|
||||
delete_account_confirm: "هل انت متاكد من انك تريد حذف حسابك نهائيا؟ لايمكن التراجع عن هذا العمل!"
|
||||
delete_account_confirm: "أمتأكّد من حذف حسابك للأبد؟ هذا إجراء لا عودة فيه!"
|
||||
deleted_yourself: "تم حذف حسابك بنجاح"
|
||||
delete_yourself_not_allowed: "لايمكنك حذف حسابك الان , تواصل مع المدير ليحذف حسابك "
|
||||
unread_message_count: "الرسائل"
|
||||
@ -709,17 +709,17 @@ ar:
|
||||
choose: "ادخل رمز سري"
|
||||
change_about:
|
||||
title: "تعديل ملفي الشخصي"
|
||||
error: "حدث خطأ عند تغيير هذه القيمة."
|
||||
error: "حدث عطل في تغيير هذه القيمة."
|
||||
change_username:
|
||||
title: "تغيير اسم المستخدم"
|
||||
confirm: "عند تغييرك أسم المستخدم, جميع الاقتباسات السابقة من مشاركاتك و ذكر اسم المستخدم الخاص بك ستصبح غير صحيحة ولا تشير الى ملفك الشخصي. هل انت متأكد تماماً من اختيارك؟"
|
||||
taken: "عذراً، هذا الإسم مُستخدم مسبقاً."
|
||||
error: "حدث خطأ عند تغيير اسم المستخدم."
|
||||
invalid: "اسم المستخدم غير صالح. يجب ان يحتوي على ارقام وحروف فقط "
|
||||
taken: "نأسف، اسم المستخدم مأخوذ."
|
||||
error: "حدث عطل في تغيير اسم المستخدم."
|
||||
invalid: "اسم المستخدم غير صالح. يمكنه احتواء الأرقام والأحرف فحسب"
|
||||
change_email:
|
||||
title: "تغيير البريد الالكتروني"
|
||||
taken: "نأسف، البريد الالكتروني غير متاح."
|
||||
error: "حدث خطأ عند تغيير البريد الالكتروني. ربما يكون هذا البريد مستخدم من قبل؟"
|
||||
title: "غيّر البريد الإلكترونيّ"
|
||||
taken: "نأسف، البريد الإلكترونيّ غير متوفّر."
|
||||
error: "حدث عطل في تغيير البريد الإلكترونيّ. لربّما هناك من يستخدم هذا العنوان بالفعل؟"
|
||||
success: "لقد أرسلنا بريدا إلى هذا البريد. من فضلك اتّبع تعليمات التأكيد."
|
||||
change_avatar:
|
||||
title: "غير صورتك الشخصية"
|
||||
@ -741,7 +741,7 @@ ar:
|
||||
instructions: "سيتم وضع الخلفية في المنتصف بعرض 590px"
|
||||
email:
|
||||
title: "البريد الإلكتروني"
|
||||
instructions: "سيكون مخفي عن الجميع"
|
||||
instructions: "لا يظهر للعموم"
|
||||
ok: "سنرسل لك بريدا للتأكيد"
|
||||
invalid: "من فضلك أدخل بريدا إلكترونيا صالحا"
|
||||
authenticated: "تم توثيق بريدك الإلكتروني بواسطة {{provider}}"
|
||||
@ -761,18 +761,18 @@ ar:
|
||||
ok: "يبدو اسمك جيدا"
|
||||
username:
|
||||
title: "اسم المستخدم"
|
||||
instructions: "غير مكرر , بدون مسافات , قصير"
|
||||
instructions: "فريد دون مسافات وقصير"
|
||||
short_instructions: "يمكن للغير الإشارة إليك ب@{{username}}"
|
||||
available: "اسم المستخدم متاح."
|
||||
not_available: "غير متاح. جرّب {{suggestion}} ؟"
|
||||
not_available_no_suggestion: "غير متاح"
|
||||
too_short: "اسم المستخدم قصير جداً"
|
||||
too_long: "اسم المستخدم طويل جداً"
|
||||
too_short: "اسم المستخدم قصير جدًّا"
|
||||
too_long: "اسم المستخدم طويل جدًّا"
|
||||
checking: "يتم التاكد من توفر اسم المستخدم..."
|
||||
prefilled: "البريد الالكتروني مطابق لـ اسم المستخدم المسّجل."
|
||||
locale:
|
||||
title: "لغة الموقع."
|
||||
instructions: "اللغة المستخدمة تغيرت. لتفعيل اللغة الجديدة قم بتحديث الصفحة. "
|
||||
title: "لغة الواجهة"
|
||||
instructions: "لغة الواجهة الرّسومية. ستتغيّر عندما تُنعش الصّفحة."
|
||||
default: "(الافتراضية)"
|
||||
any: "أي"
|
||||
password_confirmation:
|
||||
@ -808,14 +808,14 @@ ar:
|
||||
every_two_weeks: "كل أسبوعين"
|
||||
include_tl0_in_digests: "في إيميلات تلخيص المحتوى إرفق مشاركات الأعضاء الجدد."
|
||||
email_in_reply_to: " بالإشعار الألكتروني إرفق مقتطفات من الردود."
|
||||
email_direct: "تلقي رسالة إلكترونية عند اقتباس مشاركة لك أو الرد على عليها أو في حالة ذكر اسمك @username"
|
||||
email_private_messages: "إرسل إشعار بالبريد الإلكتروني عندما يرسل لك شخصاً رسالة خاصة."
|
||||
email_always: "إرسل التنبيهات على بريدي الإلكتروني حتى وإن كنت متواجد في الموقع ."
|
||||
other_settings: "اخرى"
|
||||
email_direct: "أرسل إليّ بريدًا عندما يقتبس أحد كلامي، أو يرّد على إحدى مشاركاتي، أو يذكر @اسمي أو يدعوني إلى أحد المواضيع"
|
||||
email_private_messages: "أرسل إليّ بريدًا عندما يبعث أحدهم رسالة إليّ"
|
||||
email_always: "أرسل إليّ الإخطارات عبر البريد حتّى ولو كنت متّصلًا"
|
||||
other_settings: "أخرى"
|
||||
categories_settings: "الأقسام"
|
||||
new_topic_duration:
|
||||
label: " \nإعتبر المواضيع جديدة في حال"
|
||||
not_viewed: "لم أشاهدها بعد. "
|
||||
not_viewed: "لم أطالعها بعد"
|
||||
last_here: "تم إنشائها منذ اخر زيارة لي. "
|
||||
after_1_day: "أُنشئت في اليوم الماضي"
|
||||
after_2_days: "أُنشئت في اليومين الماضيين"
|
||||
@ -826,13 +826,13 @@ ar:
|
||||
never: "ابداً"
|
||||
immediately: "حالاً"
|
||||
after_30_seconds: "بعد 30 ثانية"
|
||||
after_1_minute: "بعد 1 دقيقة"
|
||||
after_2_minutes: "بعد 2 دقائق"
|
||||
after_1_minute: "بعد دقيقة واحدة"
|
||||
after_2_minutes: "بعد دقيقتين"
|
||||
after_3_minutes: "بعد 3 دقائق"
|
||||
after_4_minutes: "بعد 4 دقائق"
|
||||
after_5_minutes: "بعد 5 دقائق"
|
||||
after_10_minutes: "بعد 10 دقائق"
|
||||
notification_level_when_replying: "عندما أشارك في موضوع، اجعل ذلك الموضوع"
|
||||
notification_level_when_replying: "إن شاركت في موضوع ما، اجعله"
|
||||
invited:
|
||||
search: "نوع البحث عن الدعوات"
|
||||
title: "دعوة"
|
||||
@ -867,32 +867,32 @@ ar:
|
||||
account_age_days: "عمر الحساب بالأيام"
|
||||
create: "أرسل دعوة"
|
||||
generate_link: "انسخ رابط الدعوة"
|
||||
link_generated: "تم إنشاء رابط الدعوة بنجاح!"
|
||||
valid_for: "رابط الدعوة صحيح فقط للبريد الإلكتروني %{email}"
|
||||
link_generated: "وُلّد رابط الدّعوة بنجاح!"
|
||||
valid_for: "رابط الدعوة صالح للبريد الإلكترونيّ هذا فقط: %{email}"
|
||||
bulk_invite:
|
||||
none: "لم تقم بدعوة احد الى هنا حتى الآن، يمكنك إرسال دعوه واحدة او مجموعة دعوات عن طريق ملف CSV."
|
||||
text: "الدعوة من ملف"
|
||||
success: "رُفع الملف بنجاح. سيصلك إشعارا عبر رسالة عند اكتمال العملية."
|
||||
error: "عذراً، الملف يجب ان يكون على صيغة CSV."
|
||||
error: "نأسف، يجب أن يكون الملفّ بنسق CSV."
|
||||
password:
|
||||
title: "الرمز السري. "
|
||||
too_short: "الرمز السري قصير جداً."
|
||||
common: "الرمز السري ضعيف!"
|
||||
same_as_username: "الرمز السري مُطابق لاسم المستخدم."
|
||||
same_as_email: "الرمز السري مطابق للبريد الإلكتروني."
|
||||
ok: "الرمز السري جيّد. "
|
||||
title: "كلمة السّرّ"
|
||||
too_short: "كلمة السّرّ قصيرة جدًّا."
|
||||
common: "كلمة السّرّ هذه شائعة."
|
||||
same_as_username: "كلمة السّرّ تطابق اسم المستخدم."
|
||||
same_as_email: "كلمة السّر تطابق البريد الإلكترونيّ."
|
||||
ok: "تبدو كلمة السّر جيّدة."
|
||||
instructions: "يجب أن تكون على الأقل %{count} أحرف او أرقام"
|
||||
summary:
|
||||
title: "ملخص"
|
||||
stats: "إحصائيات العضو:"
|
||||
stats: "إحصائيّات"
|
||||
time_read: "< الوقت الذي امضاه في الموقع"
|
||||
topic_count:
|
||||
zero: "< إجمالي المواضيع"
|
||||
one: "< إجمالي المواضيع"
|
||||
two: "< إجمالي المواضيع"
|
||||
few: "< إجمالي المواضيع"
|
||||
many: "< إجمالي المواضيع"
|
||||
other: "< إجمالي المواضيع"
|
||||
zero: "عدد المواضيع المنشأة"
|
||||
one: "عدد المواضيع المنشأة"
|
||||
two: "عدد المواضيع المنشأة"
|
||||
few: "عدد المواضيع المنشأة"
|
||||
many: "عدد المواضيع المنشأة"
|
||||
other: "عدد المواضيع المنشأة"
|
||||
post_count:
|
||||
zero: "< إجمالي المواضيع"
|
||||
one: " < اجمالي المشاركات"
|
||||
@ -936,20 +936,20 @@ ar:
|
||||
many: "المفضلة"
|
||||
other: "المفضلة"
|
||||
top_replies: "أفضل الردود"
|
||||
no_replies: "لَم ينشر أي رد بعد."
|
||||
more_replies: "المزيد من الردود"
|
||||
no_replies: "لا ردود بعد."
|
||||
more_replies: "ردود أخرى"
|
||||
top_topics: "أفضل المواضيع"
|
||||
no_topics: "لَم ينشر أي موضوع بعد. "
|
||||
more_topics: "المزيد من المواضيع"
|
||||
top_badges: "افضل الاوسمه"
|
||||
no_badges: "لم يحصل على اي وسام بعد. "
|
||||
no_topics: "لا مواضيع بعد."
|
||||
more_topics: "مواضيع أخرى"
|
||||
top_badges: "أفضل الشّارات"
|
||||
no_badges: "لا شارات بعد."
|
||||
more_badges: "أوسمة أخرى .. "
|
||||
top_links: "أفضل الروابط التي اضافها:"
|
||||
no_links: "ليس هناك اي روابط بعد."
|
||||
top_links: "أفضل الرّوابط"
|
||||
no_links: "لا روابط بعد."
|
||||
most_liked_by: "الأعضاء المُعجبين بمشاركاتِه:"
|
||||
most_liked_users: "الأعضاء الذين تعجبهُ مشاركاتِهم:"
|
||||
most_replied_to_users: "الأعضاء الذين يتفاعل معهم غالباً:"
|
||||
no_likes: "لم يتلقى أي إعجاب بعد."
|
||||
no_likes: "لا إعجابات بعد."
|
||||
associated_accounts: "الحسابات المرتبطة"
|
||||
ip_address:
|
||||
title: "عنوان IP الأخير"
|
||||
@ -988,11 +988,11 @@ ar:
|
||||
again: "أعد المحاولة"
|
||||
fixed: "تحميل"
|
||||
close: "اغلاق"
|
||||
assets_changed_confirm: "لقد تم تحديث الموقع قبل قليل، هل تريد تحديث الصفحة لتصفح الموقع بآخر نسخة؟ "
|
||||
assets_changed_confirm: "حُدّث الموقع لتوّه. أتريد إنعاش الصّفحة ورؤية أحدث إصدارة؟"
|
||||
logout: "لقد تم تسجيل خروجك"
|
||||
refresh: "تحديث"
|
||||
read_only_mode:
|
||||
enabled: "الموقع حالياً في وضع القراءة فقط، هذا يعني انه لا يمكنك الآن إنشاء مواضيع او إضافة ردود او تقديم إعجاب بمشاركة. "
|
||||
enabled: "هذا الموقع في وضع القراءة فقط. نأمل أن تتابع تصفّحه، ولكنّ الرّدّ، والإعجاب وغيرها من إجراءات معطّلة حاليًّا."
|
||||
login_disabled: "تسجيل الدخول معطّل عندما يكون الموقع في حالة القراءة فقط"
|
||||
logout_disabled: "تسجيل الخروج معطّل عندما يكون الموقع في حاله القراءة فقط"
|
||||
too_few_topics_and_posts_notice: "دعونا <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>الحصول على هذه المناقشة بدأت!</a> يوجد حاليا<strong>%{currentTopics} / %{requiredTopics}</strong> المواضيع و <strong>%{currentPosts} / %{requiredPosts}</strong> المشاركات. الزوار الجدد بحاجة إلى بعض الأحاديث لقراءة والرد على."
|
||||
@ -1001,7 +1001,7 @@ ar:
|
||||
logs_error_rate_notice:
|
||||
reached: "<b>%{relativeAge}</b> – <a href='%{url}' target='_blank'>%{rate}</a> وصل الحد الأعلى لإعدادت الموقع %{siteSettingRate}."
|
||||
exceeded: "<b>%{relativeAge}</b> – <a href='%{url}' target='_blank'>%{rate}</a> يتجاوز الحد الأعلى لإعدادت الموقع %{siteSettingRate}."
|
||||
learn_more: "اقرأ اكثر .."
|
||||
learn_more: "اطّلع على المزيد..."
|
||||
all_time: 'المجموع'
|
||||
all_time_desc: 'عدد المواضيع المنشأة'
|
||||
year: 'عام'
|
||||
@ -1011,11 +1011,11 @@ ar:
|
||||
week: 'أسبوع'
|
||||
week_desc: ' المواضيع التي كتبت خلال 7 أيام الماضية'
|
||||
day: 'يوم'
|
||||
first_post: الموضوع الأول
|
||||
first_post: أوّل مشاركة
|
||||
mute: كتم
|
||||
unmute: إلغاء الكتم
|
||||
last_post: أخر مشاركة
|
||||
last_reply_lowercase: آخر رد
|
||||
last_post: آخر مشاركة
|
||||
last_reply_lowercase: آخر ردّ
|
||||
replies_lowercase:
|
||||
zero: الردود
|
||||
one: الردود
|
||||
@ -1037,57 +1037,57 @@ ar:
|
||||
enable: 'لخّص هذا الموضوع'
|
||||
disable: 'أظهر كل المشاركات'
|
||||
deleted_filter:
|
||||
enabled_description: "هذا الموضوع يحوي على مشاركات محذوفة تم اخفائها "
|
||||
disabled_description: "المشاركات المحذوفة في هذا الموضوع ممكن مشاهدتها "
|
||||
enable: "إخفاء المشاركات المحذوفة"
|
||||
disable: "عرض المشاركات المحذوفة"
|
||||
enabled_description: "في هذا الموضوع مشاركات محذوفة قد أُخفيت."
|
||||
disabled_description: "المشاركات المحذوفة في الموضوع معروضة."
|
||||
enable: "أخفِ المشاركات المحذوفة"
|
||||
disable: "أظهر المشاركات المحذوفة"
|
||||
private_message_info:
|
||||
title: " رسالة خاصة"
|
||||
invite: " إدعو اخرين"
|
||||
remove_allowed_user: "هل تريد حقا ازالة {{name}} من الرسائل الخاصة ؟"
|
||||
remove_allowed_group: "هل تريد حقا ازالة {{name}} من هذه الرسالة ?"
|
||||
remove_allowed_user: "أمتأكّد من إزالة {{name}} من هذه الرّسالة؟"
|
||||
remove_allowed_group: "أمتأكّد من إزالة {{name}} من هذه الرّسالة؟"
|
||||
email: 'البريد الإلكتروني'
|
||||
username: 'إسم المستخدم'
|
||||
last_seen: 'شوهدت'
|
||||
created: 'مكتوبة'
|
||||
created_lowercase: 'نُشر منذ'
|
||||
created_lowercase: 'أُنشئ'
|
||||
trust_level: 'مستوى التقة'
|
||||
search_hint: 'اسم مستخدم او بريد الكتروني او عنوان ايبي'
|
||||
search_hint: 'اسم المستخدم، أو بريد إلكترونيّ أو عنوان IP'
|
||||
create_account:
|
||||
disclaimer: "التّسجيل يعني موافقتك على <a href='{{privacy_link}}'>سياسة الخصوصيّة</a> و<a href='{{tos_link}}'>بنود الخدمة</a>."
|
||||
title: "إنشاء حساب جديد"
|
||||
failed: "حدث خطأ ما, ربما بريدك الالكتروني مسجل مسبقا, جرب رابط نسيان كلمة المرور "
|
||||
forgot_password:
|
||||
title: " إعادة تعيين كلمة المرور"
|
||||
action: "لقد نسيت رمزي السري"
|
||||
invite: "ادخل اسم مستخدمك او بريدك الالكتروني وسنقوم بإرسال اعاذة ضبط كلمة المرور على بريدك"
|
||||
action: "نسيتُ كلمة السّرّ"
|
||||
invite: "أدخل اسم المستخدم أو البريد الإلكترونيّ وسنرسل بريدًا لتصفير كلمة السّرّ."
|
||||
reset: " إعادة تعين الرمز السري"
|
||||
complete_username: "اذا كان اسم المسنخدم موجود <b>%{username}</b>, سيتم ارسال رسالة لبريدك لإعادة ضبط كلمة المرور "
|
||||
complete_email: "اذا كان الحساب متطابق <b>%{email}</b>, سوف تستلم بريد الالكتروني يحوي على التعليمات لإعادة ضبط كلمة المرور"
|
||||
complete_username_found: "وجدنا حساب متطابق مع المستخدم <b>%{username}</b>, سوف تستلم بريد الالكتروني يحوي على التعليمات لإعادة ضبط كلمة المرور"
|
||||
complete_email_found: "وجدنا حساب متطابق مع <b>%{email}</b>, سوف تستلم بريد الالكتروني يحوي على التعليمات لإعادة ضبط كلمة المرور"
|
||||
complete_username: "إن تطابق حساب ما مع اسم المستخدم <b>%{username}</b>، فيفترض أن تستلم قريبًا بريدًا به إرشادات تصفير كلمة السّرّ."
|
||||
complete_email: "إن تطابق حساب ما مع <b>%{email}</b>، فيفترض أن تستلم قريبًا بريدًا به إرشادات تصفير كلمة السّرّ."
|
||||
complete_username_found: "عثرنا على حساب يطابق اسم المستخدم <b>%{username}</b>. يفترض أن تستلم قريبًا بريدًا به إرشادات تصفير كلمة السر."
|
||||
complete_email_found: "عثرنا على حساب يطابق <b>%{email}</b>. يفترض أن تستلم قريبًا بريدًا به إرشادات تصفير كلمة السر."
|
||||
complete_username_not_found: "لايوجد حساب متطابق مع هذا المستخدم <b>%{username}</b>"
|
||||
complete_email_not_found: "لايوجد حساب متطابق مع <b>%{email}</b>"
|
||||
login:
|
||||
title: "تسجيل الدخول"
|
||||
username: "المستخدم"
|
||||
password: "كلمة المرور"
|
||||
password: "كلمة السّرّ"
|
||||
email_placeholder: "البريد الإلكتروني أو اسم المستخدم"
|
||||
caps_lock_warning: "حساسية الأحرف الإنقليزية Caps Lock في وضع التشغيل"
|
||||
caps_lock_warning: "مفتاح Caps Lock مفعّل"
|
||||
error: "خطأ مجهول"
|
||||
rate_limit: "الرجاء اﻹنتظارقبل محاولة تسجيل الدخول مجدداً."
|
||||
blank_username_or_password: "من فضلك أدخل اسم المستخدم أو البريد الإلكتروني وكلمة المرور."
|
||||
reset_password: 'صفّر كلمة المرور'
|
||||
blank_username_or_password: "رجاءً أدخل اسم المستخدم (أو البريد الإلكترونيّ) وكلمة السّرّ."
|
||||
reset_password: 'صفّر كلمة السّرّ'
|
||||
logging_in: "...تسجيل الدخول "
|
||||
or: "أو "
|
||||
authenticating: "جارِ التحقق .. "
|
||||
awaiting_confirmation: "ما زال حسابك غير مفعّل، استخدم رابط نسيان كلمة المرور لإرسال بريد إلكتروني تفعيلي آخر."
|
||||
awaiting_approval: "لم يوافق أحد أعضاء الطاقم على حسابك بعد. سيُرسل إليك بريد حالما يتم ذلك."
|
||||
awaiting_approval: "لم يوافق أيّ من أعضاء الطّاقم على حسابك بعد. سيُرسل إليك بريد حالما يتمّ ذلك."
|
||||
requires_invite: "المَعذرة، الوصول لهذا الموقع خاص بالمدعويين فقط."
|
||||
not_activated: "لا يُمكنك تسجيل الدخول حتى تقوم بتفعيل الحساب. لقد سبق و أن أرسلنا بريد إلكتروني إلى <b>{{sentTo}}</b> لتفعيل حسابك. الرجاء اتباع التعليمات المرسلة لتفعيل الحساب."
|
||||
not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول من خلال هذا العنوان الرقمي - IP."
|
||||
admin_not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول كمدير من خلال هذا العنوان الرقمي - IP."
|
||||
resend_activation_email: "اضغط هنا لإرسال رسالة إلكترونية أخرى لتفعيل الحساب."
|
||||
sent_activation_email_again: "لقد قمنا بإرسال رسالة التفعيل مرة أُخرى إلى <b>{{currentEmail}}</b> قد يستغرق وصول الإيميل عدة دقائق. لاتنسى أن تتأكد من مجلد Spam أيضاً. "
|
||||
resend_activation_email: "انقر هنا لإرسال بريد التّفعيل مرّة أخرى."
|
||||
sent_activation_email_again: "لقد أرسلنا بريد تفعيل آخر إلى <b>{{currentEmail}}</b>. قد يستغرق وصوله بضعة دقائق. تحقّق من فحص مجلّد السّخام."
|
||||
to_continue: "من فضلك قم بتسجيل دُخولك"
|
||||
preferences: "يتوجب عليك تسجيل الدخول لتغيير إعداداتك الشخصية."
|
||||
forgot: "لا أذكر معلومات حسابي"
|
||||
@ -1153,14 +1153,21 @@ ar:
|
||||
saved_local_draft_tip: "حُفظ محليا"
|
||||
similar_topics: "موضوعك يشابه..."
|
||||
drafts_offline: "مسودات محفوظة "
|
||||
group_mentioned:
|
||||
zero: "الإشارة إلى {{group}} تعني <a href='{{group_link}}'>عدم إخطار أحد</a>. أمتأكّد؟"
|
||||
one: "الإشارة إلى {{group}} تعني إخطار <a href='{{group_link}}'>شخص واحد</a>. أمتأكّد؟"
|
||||
two: "الإشارة إلى {{group}} تعني إخطار <a href='{{group_link}}'>شخصين</a>. أمتأكّد؟"
|
||||
few: "الإشارة إلى {{group}} تعني إخطار <a href='{{group_link}}'>{{count}} أشخاص</a>. أمتأكّد؟"
|
||||
many: "الإشارة إلى {{group}} تعني إخطار <a href='{{group_link}}'>{{count}} شخصًا</a>. أمتأكّد؟"
|
||||
other: "الإشارة إلى {{group}} تعني إخطار <a href='{{group_link}}'>{{count}} شخص</a>. أمتأكّد؟"
|
||||
duplicate_link: "يبدو أن الرابط المشير إلى <b>{{domain}}</b> قد نشره <b>@{{username}}</b> في الموضوع في <a href='{{post_url}}'>رد له {{ago}}</a> – أمتأكد من نشره مجددا؟"
|
||||
error:
|
||||
title_missing: "العنوان مطلوب"
|
||||
title_too_short: "العنوان يجب أن يكون اكثر {{min}} حرف"
|
||||
title_too_long: "العنوان يجب أن لا يكون أكثر من {{max}} حرف"
|
||||
post_missing: "لا يمكن للمشاركة أن تكون خالية"
|
||||
post_missing: "لا يمكن أن تكون المشاركة خالية"
|
||||
post_length: "التعليق يجب أن يكون أكثر {{min}} حرف"
|
||||
try_like: 'هل جربت زر <i class="fa fa-heart"></i> ؟'
|
||||
try_like: 'هل جرّبت زرّ <i class="fa fa-heart"></i>؟'
|
||||
category_missing: "يجب اختيار احد الأقسام"
|
||||
save_edit: "حفظ التحرير"
|
||||
reply_original: "التعليق على الموضوع الاصلي"
|
||||
@ -1169,17 +1176,17 @@ ar:
|
||||
cancel: "إلغاء"
|
||||
create_topic: "إنشاء موضوع"
|
||||
create_pm: "رسالة"
|
||||
title: "او اضغط على Ctrl+Enter"
|
||||
title: "أو اضغط Ctrl+Enter"
|
||||
users_placeholder: "أضف مستخدما"
|
||||
title_placeholder: "بجملة واحدة، ما الذي تود النقاش عنه؟"
|
||||
edit_reason_placeholder: "لماذا تعدّل النص؟"
|
||||
show_edit_reason: "(أضف سبب التعديل)"
|
||||
reply_placeholder: "اكتب ما تريد هنا. استخدم Markdown، أو BBCode، أو HTML للتنسيق. اسحب الصور أو ألصقها."
|
||||
view_new_post: "الاطلاع على أحدث مشاركاتك"
|
||||
view_new_post: "اعرض المشاركة الجديدة."
|
||||
saving: "يحفظ"
|
||||
saved: "حُفظ!"
|
||||
saved_draft: "جاري إضافة المسودة. اضغط للاستئناف"
|
||||
uploading: "جارِ التحميل..."
|
||||
uploading: "يرفع..."
|
||||
show_preview: 'أظهر المعاينة »'
|
||||
hide_preview: '« أخفِ المعاينة'
|
||||
quote_post_title: "اقتبس كامل المشاركة"
|
||||
@ -1209,11 +1216,11 @@ ar:
|
||||
toggler: "اخف او اظهر صندوق التحرير"
|
||||
modal_ok: "حسنا"
|
||||
modal_cancel: "ألغِ"
|
||||
cant_send_pm: "آسفون، لا يمكنك إرسال الرسائل إلى %{username} ."
|
||||
cant_send_pm: "نأسف، لا يمكنك إرسال الرّسائل إلى %{username}."
|
||||
yourself_confirm:
|
||||
title: "هل نسيت أن تضيف المرسل اليهم؟"
|
||||
title: "أنسيت إضافة المستلمين؟"
|
||||
body: "حاليا هذة الرسالة مرسلة اليك فقط!"
|
||||
admin_options_title: "اختياري اضافة اعدادات الموضوع"
|
||||
admin_options_title: "إعدادات الطّاقم الاختياريّة لهذا الموضوع"
|
||||
auto_close:
|
||||
label: "وقت الإغلاق التلقائي للموضوع"
|
||||
error: "من فضلك أدخل قيمة صالحة."
|
||||
@ -1268,10 +1275,10 @@ ar:
|
||||
private_message: 'أرسل {{username}} إليك رسالة خاصة في "{{topic}}" - {{site_title}}'
|
||||
linked: '{{username}} رتبط بمشاركتك من "{{topic}}" - {{site_title}}'
|
||||
upload_selector:
|
||||
title: "اضف صورة"
|
||||
title_with_attachments: "اضف صورة او ملف"
|
||||
from_my_computer: "عن طريق جهازي"
|
||||
from_the_web: "عن طريق الويب"
|
||||
title: "أضف صورة"
|
||||
title_with_attachments: "أضف صورة أو ملفّ"
|
||||
from_my_computer: "من جهازي"
|
||||
from_the_web: "من الوبّ"
|
||||
remote_tip: "رابط لصورة"
|
||||
remote_tip_with_attachments: "رابط لصورة أو ملف {{authorized_extensions}}"
|
||||
local_tip: "إختر صور من جهازك ."
|
||||
@ -1289,7 +1296,7 @@ ar:
|
||||
most_liked: "الأكثر إعجابا"
|
||||
select_all: "أختر الكل"
|
||||
clear_all: "إلغ إختيار الكل"
|
||||
too_short: "مصطلح البحث قصير جدا."
|
||||
too_short: "عبارة البحث قصيرة جدًّا."
|
||||
result_count:
|
||||
zero: "لا نتائج ل <span class='term'>\"{{term}}\"</span>"
|
||||
one: "نتيجة واحدة ل <span class='term'>\"{{term}}\"</span>"
|
||||
@ -1301,16 +1308,16 @@ ar:
|
||||
no_results: "لا نتائج."
|
||||
no_more_results: "لا نتائج أخرى."
|
||||
searching: "يبحث..."
|
||||
post_format: "#{{post_number}} بواسطة {{username}}"
|
||||
post_format: "#{{post_number}} كتبها {{username}}"
|
||||
context:
|
||||
user: "البحث عن مواضيع @{{username}}"
|
||||
user: "ابحث عن مواضيع @{{username}}"
|
||||
category: "ابحث في فئة #{{category}}"
|
||||
topic: "ابحث في هذا الموضوع"
|
||||
private_messages: "البحث في الرسائل الخاصة"
|
||||
advanced:
|
||||
title: البحث التفصيلي
|
||||
title: بحث متقدّم
|
||||
posted_by:
|
||||
label: نُشرت بواسطة
|
||||
label: نشرها
|
||||
in_category:
|
||||
label: في الفئة
|
||||
in_group:
|
||||
@ -1320,26 +1327,28 @@ ar:
|
||||
with_tags:
|
||||
label: مع أوسمة
|
||||
filters:
|
||||
label: استرجع فقط المواضيع/المشاركات التي...
|
||||
likes: أعجبني
|
||||
posted: شاركت في
|
||||
label: ابحث عن المواضيع والمنشورات التي...
|
||||
likes: أعجبتني
|
||||
posted: شاركت فيها
|
||||
watching: التي أراقبها
|
||||
tracking: التي اتابعها
|
||||
private: من ضمن رسائلي
|
||||
bookmarks: أضفت للمفضلات
|
||||
private: موجودة في رسائلي
|
||||
bookmarks: أضفتها للعلامات
|
||||
first: تلك المشاركة الاولى
|
||||
seen: قرأتها
|
||||
unseen: لم أقرأها
|
||||
statuses:
|
||||
label: عندما المواضيع
|
||||
label: بشرط أن تكون المواضيع
|
||||
open: مفتوحة
|
||||
closed: مغلقة
|
||||
archived: محفوظة
|
||||
noreplies: لا تحتوي على رد
|
||||
single_user: تحتوي على مستخدم واحد
|
||||
archived: مؤرشفة
|
||||
noreplies: ليس فيها أيّ ردّ
|
||||
single_user: تحتوي مستخدما واحدا
|
||||
post:
|
||||
count:
|
||||
label: عدد المشاركات الأدنى
|
||||
time:
|
||||
label: ا’رْسِلت
|
||||
label: المُرسَلة
|
||||
before: قبل
|
||||
after: بعد
|
||||
hamburger_menu: "انتقل إلى قائمة مواضيع أو فئة أخرى"
|
||||
@ -1375,7 +1384,6 @@ ar:
|
||||
few: "حددت <b>{{count}}</b> مواضيع."
|
||||
many: "حددت <b>{{count}}</b> موضوعا."
|
||||
other: "حددت <b>{{count}}</b> موضوع."
|
||||
change_tags: "غيّر الوسوم"
|
||||
choose_new_tags: "اختر وسوم جديدة لهذه المواضيع:"
|
||||
changed_tags: "تغيرت وسوم هذه المواضيع."
|
||||
none:
|
||||
@ -1408,12 +1416,12 @@ ar:
|
||||
stop_notifications: "ستستقبل الأن إشعارات أقل لـ<strong>{{title}}</strong>"
|
||||
change_notification_state: "حالة إشعارك الحالي هي "
|
||||
filter_to:
|
||||
zero: "عدد الردود في الموضوع"
|
||||
one: "رد في الموضوع"
|
||||
two: "الردود في الموضوع"
|
||||
few: "الردود في الموضوع"
|
||||
many: "الردود في الموضوع "
|
||||
other: "الردود في الموضوع "
|
||||
zero: "لا مشاركات في الموضوع"
|
||||
one: "مشاركة واحدة في الموضوع"
|
||||
two: "مشاركتان في الموضوع"
|
||||
few: "{{count}} مشاركات في الموضوع"
|
||||
many: "{{count}} مشاركة في الموضوع"
|
||||
other: "{{count}} مشاركة في الموضوع"
|
||||
create: 'موضوع جديد'
|
||||
create_long: 'كتابة موضوع جديد'
|
||||
private_message: 'أرسل رسالة خاصة'
|
||||
@ -1449,7 +1457,7 @@ ar:
|
||||
title: "فشل تحميل الموضوع"
|
||||
description: "آسفون، تعذر علينا تحميل هذا الموضوع، قد يرجع ذلك إلى مشكلة بالاتصال. من فضلك حاول مجددا. أخبرنا بالمشكلة إن استمر حدوثها."
|
||||
not_found:
|
||||
title: "لم يتم العثور على الموضوع"
|
||||
title: "لم يُعثر على الموضوع"
|
||||
description: "آسفون، لم نجد هذا الموضوع. ربما أزاله أحد المشرفين؟"
|
||||
total_unread_posts:
|
||||
zero: "لا مشاركات غير مقروءة في هذا الموضوع"
|
||||
@ -1485,14 +1493,14 @@ ar:
|
||||
toggle_information: "أظهر/أخف تفاصيل الموضوع"
|
||||
read_more_in_category: "أتريد قراءة المزيد؟ تصفح المواضيع الأخرى في {{catLink}} أو {{latestLink}}."
|
||||
read_more: "أتريد قراءة المزيد؟ {{catLink}} أو {{latestLink}}."
|
||||
read_more_MF: "{UNREAD, plural, zero {} one {تبقى {BOTH, select, true {<a href='/unread'>موضوع واحد غير مقروء</a> و} false {<a href='/unread'>موضوع واحد غير مقروء</a>} other {}}} two {تبقى {BOTH, select, true {<a href='/unread'>موضوعان غير مقروءان</a> و} false {<a href='/unread'>موضوعان غير مقروءان</a>} other {}}} few {تبقت {BOTH, select, true {<a href='/unread'># مواضيع غير مقروءة</a> و} false {<a href='/unread'># مواضيع غير مقروءة</a>} other {}}} many {تبقى {BOTH, select, true {<a href='/unread'># موضوعا غير مقروء</a> و} false {<a href='/unread'># موضوعا غير مقروء</a>} other {}}} other {تبقى {BOTH, select, true {<a href='/unread'># موضوع غير مقروء</a> و} false {<a href='/unread'># موضوع غير مقروء</a>} other {}}}}{NEW, plural, zero {} one {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'>موضوع واحد جديد</a>} two {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'>موضوعان جديدان</a>} few {{UNREAD, plural, zero {تبقت } one {} two {} few {} many {} other {}}<a href='/new'># مواضيع جديدة</a>} many {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'># موضوعا جديدا</a>} other {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'># موضوع جديد</a>}}، أو {CATEGORY, select, true {تصفّح المواضيع الأخرى في {catLink}} false {{latestLink}} other {}}"
|
||||
read_more_MF: "{UNREAD, plural, zero {} one {تبقى <a href='/unread'>موضوع واحد غير مقروء</a>} two {تبقى <a href='/unread'>موضوعان غير مقروءان</a>} few {تبقت <a href='/unread'># مواضيع غير مقروءة</a>} many {تبقى <a href='/unread'># موضوعا غير مقروء</a>} other {تبقى <a href='/unread'># موضوع غير مقروء</a>}}{BOTH, select, true { و} false {} other {}}{NEW, plural, zero {} one {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'>موضوع واحد جديد</a>} two {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'>موضوعان جديدان</a>} few {{UNREAD, plural, zero {تبقت } one {} two {} few {} many {} other {}}<a href='/new'># مواضيع جديدة</a>} many {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'># موضوعا جديدا</a>} other {{UNREAD, plural, zero {تبقى } one {} two {} few {} many {} other {}}<a href='/new'># موضوع جديد</a>}}، أو {CATEGORY, select, true {تصفّح المواضيع الأخرى في {catLink}} false {{latestLink}} other {}}"
|
||||
browse_all_categories: تصفّح كل الأقسام
|
||||
view_latest_topics: اعرض أحدث المواضيع
|
||||
suggest_create_topic: لمَ لا تكتب موضوعًا؟
|
||||
jump_reply_up: انتقل إلى أول رد
|
||||
jump_reply_down: انتقل إلى آخر رد
|
||||
deleted: "الموضوع محذوف"
|
||||
auto_close_notice: "سيُغلق الموضوع آليا بعد %{timeLeft}."
|
||||
auto_close_notice: "سيُغلق الموضوع آليًّا %{timeLeft}."
|
||||
auto_close_notice_based_on_last_post: "سيُغلق الموضوع بعد %{duration} من آخر رد."
|
||||
auto_close_title: 'إعدادات الإغلاق الآلي'
|
||||
auto_close_save: "احفظ"
|
||||
@ -1535,13 +1543,13 @@ ar:
|
||||
title: "مُراقب"
|
||||
description: "سيصلك إشعار لكل رد جديد لهذه الرسالة، وسيظهر عدد الردود الجديدة."
|
||||
watching:
|
||||
title: "مُراقب"
|
||||
title: "مراقَبًا"
|
||||
description: "سيصلك إشعار لكل رد جديد لهذا الموضوع، وسيظهر عدد الردود الجديدة."
|
||||
tracking_pm:
|
||||
title: "مُتابع"
|
||||
description: "سيظهر عدد الردود الجديدة لهذه الرسالة. سيصلك إشعار إن أشار أحد إلى @اسمك أو رد عليك."
|
||||
tracking:
|
||||
title: "مُتابع"
|
||||
title: "متابَعًا"
|
||||
description: "سيظهر عدد الردود الجديدة لهذا الموضوع. سيصلك إشعار إن أشار أحد إلى @اسمك أو رد عليك."
|
||||
regular:
|
||||
title: "الوضع العادي"
|
||||
@ -1641,7 +1649,7 @@ ar:
|
||||
title: 'دعوة'
|
||||
username_placeholder: "اسم المستخدم"
|
||||
action: 'أرسل دعوة'
|
||||
help: 'دعوة المستخدمين لهذا الموضوع عن طرق البريد الإلكتروني أو الأشعارات'
|
||||
help: 'ادعُ الغير إلى هذا الموضوع عبر البريد الإلكترونيّ أو الإشعارات'
|
||||
to_forum: "سيتم ارسال رسالة بريد الكتروني ﻷصدقائك للمشاركة في الموقع , هذه العملية لا تتطلب تسجيل الدخول ."
|
||||
sso_enabled: "أدخل اسم مَن تريد دعوته إلى هذا الموضوع."
|
||||
to_topic_blank: "أدخل اسم مَن تريد دعوته إلى هذا الموضوع أو بريده."
|
||||
@ -1686,7 +1694,7 @@ ar:
|
||||
many: "الرجاء اختيار الموضوع الذي تود نقل الـ<b>{{count}}</b> مشاركة إليه."
|
||||
other: "الرجاء اختيار الموضوع الذي تود نقل الـ<b>{{count}}</b> مشاركة إليه."
|
||||
merge_posts:
|
||||
title: "قم بدمج المشاركات المختارة"
|
||||
title: "ادمج المشاركات المحدّدة"
|
||||
action: "ادمج المشاركات المحددة"
|
||||
error: "حدث خطأ في دمج المشاركات المحددة."
|
||||
change_owner:
|
||||
@ -1708,7 +1716,7 @@ ar:
|
||||
action: "تغيير الطابع الزمني"
|
||||
invalid_timestamp: "الطابع الزمني لا يمكن أن يكون في المستقبل."
|
||||
error: "هناك خطأ في نغيير الطابع الزمني للموضوع."
|
||||
instructions: "من فضلك اختر الطابع الزمني الجديد للموضوع. ستُحدث المشاركات فيه لألا يختلف فرق تاريخ نشرها ووقته."
|
||||
instructions: "من فضلك اختر الطابع الزمني الجديد للموضوع. ستُحدّث المشاركات فيه لألا يختلف فرق تاريخ نشرها ووقته."
|
||||
multi_select:
|
||||
select: 'حددها'
|
||||
selected: 'محددة ({{count}})'
|
||||
@ -1733,7 +1741,7 @@ ar:
|
||||
post_number: "المشاركة {{number}}"
|
||||
last_edited_on: "آخر تعديل على المشاركة في "
|
||||
reply_as_new_topic: "التعليق على الموضوع الاصلي"
|
||||
continue_discussion: "إكمال النقاش على {{postLink}}"
|
||||
continue_discussion: "إكمالا للنّقاش في {{postLink}}:"
|
||||
follow_quote: "الذهاب إلى المشاركة المقتبسة"
|
||||
show_full: "عرض كامل المشاركة"
|
||||
show_hidden: 'عرض المحتوى المخفي.'
|
||||
@ -1768,20 +1776,20 @@ ar:
|
||||
many: "{{count}} إعجابا"
|
||||
other: "{{count}} إعجاب"
|
||||
has_likes_title:
|
||||
zero: "لم تعجب هذه المشاركة أحد"
|
||||
zero: "لم تُعجب هذه المشاركة أحد"
|
||||
one: "أعجبت هذه المشاركة شخصا واحدا"
|
||||
two: "أعجبت هذه المشاركة شخصين"
|
||||
few: "أعجبت هذه المشاركة {{count}} أشخاص"
|
||||
many: "أعجبت هذه المشاركة {{count}} شخصا"
|
||||
other: "{{count}} أشخاص أعجبوا بهذه المشاركة"
|
||||
has_likes_title_only_you: "أنت أعجبت بهذه المشاركة"
|
||||
other: "أعجبت هذه المشاركة {{count}} شخص"
|
||||
has_likes_title_only_you: "أعجبتك هذه المشاركة"
|
||||
has_likes_title_you:
|
||||
zero: "أعجبتك هذه المشاركة"
|
||||
one: "أعجبت هذه المشاركة شخصا واحدا غيرك"
|
||||
one: "أعجبت هذه المشاركة شخصًا واحدًا غيرك"
|
||||
two: "أعجبت هذه المشاركة شخصين غيرك"
|
||||
few: "أعجبت هذه المشاركة {{count}} أشخاص غيرك"
|
||||
many: "أعجبت هذه المشاركة {{count}} شخصا غيرك"
|
||||
other: "أنت و {{count}} شخص أخرون أعجبتم بهذه المشاركة ."
|
||||
many: "أعجبت هذه المشاركة {{count}} شخصًا غيرك"
|
||||
other: "أعجبت هذه المشاركة {{count}} شخص غيرك"
|
||||
errors:
|
||||
create: "آسفون، حدثت مشكلة في إنشاء المشاركة. من فضلك حاول مجددا."
|
||||
edit: "آسفون، حدثت مشكلة في تعديل المشاركة. من فضلك حاول مجددا."
|
||||
@ -2027,7 +2035,7 @@ ar:
|
||||
settings: 'اعدادات'
|
||||
topic_template: "إطار الموضوع"
|
||||
tags: "الوسوم"
|
||||
tags_allowed_tags: "العلامات الوصفية التي تستخدم فقط في هذا القسم:"
|
||||
tags_allowed_tags: "ما يمكن استخدامه من وسوم في هذه الفئة فقط:"
|
||||
tags_allowed_tag_groups: "مجموعات العلامات الوصفية التي تستخدم فقط في هذا القسم:"
|
||||
tags_placeholder: "(اختياري) قائمة العلامات الوصفية المسموح بها"
|
||||
tag_groups_placeholder: "(اختياري) قائمة مجموعات العلامات الوصفية المسموح بها"
|
||||
@ -2062,7 +2070,7 @@ ar:
|
||||
auto_close_units: "ساعات"
|
||||
email_in: "تعيين بريد إلكتروني خاص:"
|
||||
email_in_allow_strangers: "قبول بريد إلكتروني من مستخدمين لا يملكون حسابات"
|
||||
email_in_disabled: "إضافة مواضيع جديدة من خلال البريد الإلكتروني موقوف في الوقت الحالي من خلال إعدادات الموقع. لتفعيل إضافة مواضيع جديدة من خلال البريد الإلكتروني,"
|
||||
email_in_disabled: "عُطّل إرسال المشاركات عبر البريد الإلكترونيّ من إعدادات الموقع. لتفعيل نشر المشاركات الجديدة عبر البريد،"
|
||||
email_in_disabled_click: 'قم بتفعيل خيار "email in" في الإعدادات'
|
||||
suppress_from_homepage: "كتم هذه الفئة من الصفحة الرئيسية"
|
||||
allow_badges_label: "السماح بالحصول على الأوسمة في هذا التصنيف"
|
||||
@ -2086,7 +2094,7 @@ ar:
|
||||
description: "ستتابع آليا كل مواضيع هذه الفئات. ستصلك إشعارات إن أشار أحدهم إلى @اسمك أو رد عليك، وسيظهر عدّاد للمشاركات الجديدة."
|
||||
regular:
|
||||
title: "منتظم"
|
||||
description: "سوف تُنبه اذا قام أحد بالاشارة لاسمك \"@name\" أو الرد عليك."
|
||||
description: "ستستقبل إشعارًا إن أشار أحد إلى @اسمك أو ردّ عليك."
|
||||
muted:
|
||||
title: "مكتومة"
|
||||
description: "لن يتم إشعارك بأي مشاركات جديدة في هذه التصنيفات ولن يتم عرضها في قائمة المواضيع المنشورة مؤخراً."
|
||||
@ -2123,12 +2131,12 @@ ar:
|
||||
custom_placeholder_notify_moderators: "ممكن تزودنا بمعلومات أكثر عن سبب عدم ارتياحك حول هذه المشاركة؟ زودنا ببعض الروابط و الأمثلة قدر الإمكان."
|
||||
custom_message:
|
||||
at_least:
|
||||
zero: "عدد الأحرف يجب ان لا يقل عن {{count}} حرف"
|
||||
one: "عدد الأحرف يجب ان لا يقل عن {{count}} حرف"
|
||||
two: "عدد الأحرف يجب ان لا يقل عن {{count}} حرف"
|
||||
few: "عدد الأحرف يجب ان لا يقل عن {{count}} حروف"
|
||||
many: "عدد الأحرف يجب ان لا يقل عن {{count}} حرف"
|
||||
other: "عدد الأحرف يجب ان لا يقل عن {{count}} حرف"
|
||||
zero: "لا تُدخل أيّ محرف"
|
||||
one: "أدخل محرفًا واحدًا على الأقلّ"
|
||||
two: "أدخل محرفين على الأقلّ"
|
||||
few: "أدخل {{count}} محارف على الأقلّ"
|
||||
many: "أدخل {{count}} محرفًا على الأقلّ"
|
||||
other: "أدخل {{count}} محرف على الأقلّ"
|
||||
flagging_topic:
|
||||
title: "شكرا لمساعدتنا في ابقاء مجتمعنا نضيفا"
|
||||
action: "التبليغ عن الموضوع"
|
||||
@ -2231,13 +2239,13 @@ ar:
|
||||
few: "الأخيرة ({{count}})"
|
||||
many: "الأخيرة ({{count}})"
|
||||
other: "الأخيرة ({{count}})"
|
||||
help: "المواضيع التي أضيفت لها ردود مؤخراً"
|
||||
help: "المواضيع التي فيها مشاركات حديثة"
|
||||
hot:
|
||||
title: "نَشط"
|
||||
help: "مختارات من مواضيع ساخنة"
|
||||
read:
|
||||
title: "المقروءة"
|
||||
help: "مواضيع قمت بقراءتها بترتيب آخر قراءة"
|
||||
help: "المواضيع التي قرأتها بترتيب آخر قراءة لها"
|
||||
search:
|
||||
title: "بحث"
|
||||
help: "بحث في كل المواضيع"
|
||||
@ -2254,7 +2262,7 @@ ar:
|
||||
few: "غير المقروءة ({{count}})"
|
||||
many: "غير المقروءة ({{count}})"
|
||||
other: "غير المقروءة ({{count}})"
|
||||
help: "مواضيع أنت تشاهدها بمشاركات غير مقروءة "
|
||||
help: "المواضيع التي تتابعها (أو تراقبها) والتي فيها مشاركات غير مقروءة"
|
||||
lower_title_with_count:
|
||||
zero: "1 غير مقررء "
|
||||
one: "1 غير مقروء"
|
||||
@ -2284,7 +2292,7 @@ ar:
|
||||
title: "مشاركاتي"
|
||||
help: "مواضيع شاركت بها "
|
||||
bookmarks:
|
||||
title: "المفضلة"
|
||||
title: "العلامات"
|
||||
help: "مواضيع قمت بتفضيلها"
|
||||
category:
|
||||
title: "{{categoryName}}"
|
||||
@ -2451,7 +2459,7 @@ ar:
|
||||
description: "ستتابع آليا كل مواضيع هذا الوسم. سيظهر عدّاد للمشاركات غير المقروءة والجديدة بجانب الموضوع."
|
||||
regular:
|
||||
title: "موضوع عادي"
|
||||
description: ".سيتم إشعارك إذا ذكر أحد ما اسمك أو رد على مشاركاتك"
|
||||
description: "ستستقبل إشعارًا إن أشار أحد إلى @اسمك أو ردّ على مشاركتك."
|
||||
muted:
|
||||
title: "مكتوم"
|
||||
description: "لن يتم إشعارك بأي جديد يخص هذا الموضوع ولن يظهرهذا الموضوع في تبويب المواضيع الغير مقروءة."
|
||||
@ -2833,7 +2841,7 @@ ar:
|
||||
undo: "تراجع"
|
||||
undo_title: "التراجع عن تغيير اللن الى اللون السابق"
|
||||
revert: "تراجع"
|
||||
revert_title: "اعادة ضبط اللون الى اللون الافتراضي للموقع"
|
||||
revert_title: "صفّر هذا اللون إلى مخطّط ألوان دسكورس الافتراضيّ."
|
||||
primary:
|
||||
name: 'الأساسي'
|
||||
secondary:
|
||||
@ -3357,7 +3365,7 @@ ar:
|
||||
enabled: تفعيل الشعار
|
||||
icon: أيقونة
|
||||
image: صورة
|
||||
icon_help: "إستخدم فئة الخط او رابط الى صورة"
|
||||
icon_help: "استخدم صنف Font Awesome أو عنوانًا إلى صورة"
|
||||
query: علامة استفهام (SQL)
|
||||
target_posts: إستعلام يستهدف المشاركات
|
||||
auto_revoke: إلغاء الاستعلام اليومي
|
||||
@ -3377,7 +3385,7 @@ ar:
|
||||
sql_error_header: "كان هناك خطأ ما في الاستعلام."
|
||||
error_help: "انظر الرابط التالي للمساعدة باستفسارات الوسام."
|
||||
bad_count_warning:
|
||||
header: "تحذير !!"
|
||||
header: "تحذير!"
|
||||
text: "هناك عينات ممنوحة ضائعة. حدث هذا عندما أعادت شارة الإستعلام user IDs أو post IDs التي لم تكن موجودة. هذا ربما بسبب نتيجة غير متوقعة في وقت لاحق - رجائا أنقر مرتين للتأكد من إستعلامك-"
|
||||
no_grant_count: "لا توجد اوسمه لتمنح "
|
||||
grant_count:
|
||||
@ -3399,10 +3407,10 @@ ar:
|
||||
add: "أضافة وجه تعبيري جديد ؟"
|
||||
name: "الأسم"
|
||||
image: "صورة"
|
||||
delete_confirm: "هل أنت متأكد من انك تريد حذف هذا :%{name}: الوجه التعبيري ؟"
|
||||
delete_confirm: "أمتأكّد من حذف الإيموجي :%{name}: هذا؟"
|
||||
embedding:
|
||||
get_started: "إذا أردت تضمين Discourse في موقع اخر، أبدأ بإضافة مضيف."
|
||||
confirm_delete: "هل انت متأكد من انك تريد حذف هذا المضيف ؟"
|
||||
confirm_delete: "أمتأكّد من حذف هذا المضيف؟"
|
||||
sample: "استخدم كود HTML التالي لموقعك لإنشاء وتضمين موضوع discourse. استبدل <b>REPLACE_ME</b> مع canonical URL لصفحة قمت بتضمينها فيه."
|
||||
title: "تضمين"
|
||||
host: "أسمع بالمضيفين"
|
||||
@ -3446,18 +3454,18 @@ ar:
|
||||
next: "التالي"
|
||||
step: "%{current} من %{total}"
|
||||
upload: "رفع"
|
||||
uploading: "جاري رفع الملفات.."
|
||||
uploading: "يرفع..."
|
||||
quit: "ربما لاحقا"
|
||||
staff_count:
|
||||
zero: "شركتك لديها %{count} من الموظفين"
|
||||
one: "مجموعتك لديها 1 موظف فقط"
|
||||
two: "مجموعتك لديها %{count} من الموظفين"
|
||||
few: "مجموعتك لديها %{count} من الموظفين"
|
||||
many: "مجموعتك لديها %{count} من الموظفين"
|
||||
other: "مجموعتك لديها %{count} من الموظفين"
|
||||
zero: "ليس في المجتمع أيّ أعضاء طاقم."
|
||||
one: "في المجتمع عضو طاقم واحد."
|
||||
two: "في المجتمع عضوا طاقم."
|
||||
few: "في المجتمع %{count} أعضاء طاقم."
|
||||
many: "في المجتمع %{count} عضو طاقم."
|
||||
other: "في المجتمع %{count} عضو طاقم."
|
||||
invites:
|
||||
add_user: "أضف"
|
||||
none_added: "لم تقم بدعوة أي موظفين. هل أنت متأكد انك تريد المتابعة؟"
|
||||
none_added: "لم تدعو أيّ عضو طاقم. أمتأكّد من المتابعة؟"
|
||||
roles:
|
||||
admin: "مدير"
|
||||
moderator: "مشرف"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user