Version bump

This commit is contained in:
Neil Lalonde 2018-02-15 17:48:49 -05:00
commit dae9d369ec
424 changed files with 6473 additions and 2263 deletions

View File

@ -36,7 +36,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.36'
gem 'onebox', '1.8.38'
gem 'http_accept_language', '~>2.0.5', require: false
@ -59,7 +59,7 @@ gem 'aws-sdk-s3', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '0.1.9'
gem 'email_reply_trimmer', '0.1.10'
# Forked until https://github.com/toy/image_optim/pull/149 is merged
gem 'discourse_image_optim', require: 'image_optim'
@ -67,9 +67,6 @@ gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
# this may end up deprecating nokogiri
gem 'oga', require: false
gem 'omniauth'
gem 'omniauth-openid'
gem 'openid-redis-store'

View File

@ -41,7 +41,6 @@ GEM
annotate (2.7.2)
activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 13.0)
ansi (1.5.0)
arel (8.0.0)
ast (2.3.0)
aws-partitions (1.24.0)
@ -91,7 +90,7 @@ GEM
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
email_reply_trimmer (0.1.9)
email_reply_trimmer (0.1.10)
ember-data-source (2.2.1)
ember-source (>= 1.8, < 3.0)
ember-handlebars-template (0.7.5)
@ -165,7 +164,7 @@ GEM
lru_redux (1.1.0)
mail (2.6.6)
mime-types (>= 1.16, < 4)
memory_profiler (0.9.8)
memory_profiler (0.9.10)
message_bus (2.1.2)
rack (>= 1.1.3)
metaclass (0.0.4)
@ -189,7 +188,7 @@ GEM
multi_xml (0.6.0)
multipart-post (2.0.0)
mustache (1.0.5)
nokogiri (1.8.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13)
nokogiri
@ -200,9 +199,6 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oga (2.10)
ast
ruby-ll (~> 2.1)
oj (3.1.0)
omniauth (1.6.1)
hashie (>= 3.4.6, < 3.6.0)
@ -232,7 +228,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.36)
onebox (1.8.38)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
@ -275,7 +271,7 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails_multisite (2.0.2)
rails_multisite (2.0.4)
activerecord (> 4.2, < 6)
railties (> 4.2, < 6)
railties (5.1.4)
@ -334,9 +330,6 @@ GEM
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-ll (2.1.2)
ansi
ast
ruby-openid (2.7.0)
ruby-prof (0.16.2)
ruby-progressbar (1.9.0)
@ -421,7 +414,7 @@ DEPENDENCIES
cppjieba_rb
discourse-qunit-rails
discourse_image_optim
email_reply_trimmer (= 0.1.9)
email_reply_trimmer (= 0.1.10)
ember-handlebars-template (= 0.7.5)
ember-rails (= 0.18.5)
ember-source (= 2.13.3)
@ -459,7 +452,6 @@ DEPENDENCIES
multi_json
mustache
nokogiri
oga
oj
omniauth
omniauth-facebook
@ -469,7 +461,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.36)
onebox (= 1.8.38)
openid-redis-store
pg (~> 0.21.0)
pry-nav

View File

@ -4,8 +4,6 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
adminTools: Ember.inject.service(),
expanded: false,
suspended: false,
tagName: 'div',
classNameBindings: [
':flagged-post',
@ -21,12 +19,7 @@ export default Ember.Component.extend({
},
removeAfter(promise) {
return promise.then(() => {
this.attrs.removePost();
}).catch(error => {
if (error._discourse_displayed) { return; }
bootbox.alert(I18n.t("admin.flags.error"));
});
return promise.then(() => this.attrs.removePost());
},
_spawnModal(name, model, modalClass) {
@ -36,7 +29,7 @@ export default Ember.Component.extend({
actions: {
removeAfter(promise) {
this.removeAfter(promise);
return this.removeAfter(promise);
},
disagree() {
@ -58,18 +51,6 @@ export default Ember.Component.extend({
filter: 'post',
post_id: this.get('flaggedPost.id')
});
},
showSuspendModal() {
let post = this.get('flaggedPost');
let user = post.get('user');
this.get('adminTools').showSuspendModal(
user,
{
post,
successCallback: result => this.set('suspended', result.suspended)
}
);
}
}
});

View File

@ -0,0 +1,32 @@
import computed from 'ember-addons/ember-computed-decorators';
const ACTIONS = ['delete', 'edit', 'none'];
export default Ember.Component.extend({
postAction: null,
postEdit: null,
@computed
penaltyActions() {
return ACTIONS.map(id => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},
editing: Ember.computed.equal('postAction', 'edit'),
actions: {
penaltyChanged() {
let postAction = this.get('postAction');
// If we switch to edit mode, jump to the edit textarea
if (postAction === 'edit') {
Ember.run.scheduleOnce('afterRender', () => {
let $elem = this.$();
let body = $elem.closest('.modal-body');
body.scrollTop(body.height());
$elem.find('.post-editor').focus();
});
}
}
}
});

View File

@ -0,0 +1,22 @@
import DiscourseURL from 'discourse/lib/url';
export default Ember.Component.extend({
classNames: ['table', 'staff-actions'],
willDestroyElement() {
this.$().off('click.discourse-staff-logs');
},
didInsertElement() {
this._super();
this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => {
let postId = $(e.target).attr('data-link-post-id');
this.store.find('post', postId).then(p => {
DiscourseURL.routeTo(p.get('url'));
});
return false;
});
}
});

View File

@ -1,26 +1,13 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import PenaltyController from 'admin/mixins/penalty-controller';
export default Ember.Controller.extend(ModalFunctionality, {
export default Ember.Controller.extend(PenaltyController, {
silenceUntil: null,
reason: null,
message: null,
silencing: false,
user: null,
post: null,
successCallback: null,
onShow() {
this.setProperties({
silenceUntil: null,
reason: null,
message: null,
silencing: false,
loadingUser: true,
post: null,
successCallback: null,
});
this.resetModal();
this.setProperties({ silenceUntil: null, silencing: false });
},
@computed('silenceUntil', 'reason', 'silencing')
@ -33,18 +20,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.get('submitDisabled')) { return; }
this.set('silencing', true);
this.get('user').silence({
silenced_till: this.get('silenceUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id')
}).then(result => {
this.send('closeModal');
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
}).catch(popupAjaxError).finally(() => this.set('silencing', false));
this.penalize(() => {
return this.get('user').silence({
silenced_till: this.get('silenceUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id'),
post_action: this.get('postAction'),
post_edit: this.get('postEdit')
});
}).finally(() => this.set('silencing', false));
}
}
});

View File

@ -1,26 +1,13 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import PenaltyController from 'admin/mixins/penalty-controller';
export default Ember.Controller.extend(ModalFunctionality, {
export default Ember.Controller.extend(PenaltyController, {
suspendUntil: null,
reason: null,
message: null,
suspending: false,
user: null,
post: null,
successCallback: null,
onShow() {
this.setProperties({
suspendUntil: null,
reason: null,
message: null,
suspending: false,
loadingUser: true,
post: null,
successCallback: null,
});
this.resetModal();
this.setProperties({ suspendUntil: null, suspending: false });
},
@computed('suspendUntil', 'reason', 'suspending')
@ -33,19 +20,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.get('submitDisabled')) { return; }
this.set('suspending', true);
this.get('user').suspend({
suspend_until: this.get('suspendUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id')
}).then(result => {
this.send('closeModal');
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
}).catch(popupAjaxError).finally(() => this.set('suspending', false));
this.penalize(() => {
return this.get('user').suspend({
suspend_until: this.get('suspendUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id'),
post_action: this.get('postAction'),
post_edit: this.get('postEdit')
});
}).finally(() => this.set('suspending', false));
}
}
});

View File

@ -0,0 +1,41 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Mixin.create(ModalFunctionality, {
reason: null,
message: null,
postEdit: null,
postAction: null,
user: null,
post: null,
successCallback: null,
resetModal() {
this.setProperties({
reason: null,
message: null,
loadingUser: true,
post: null,
postEdit: null,
postAction: 'delete',
before: null,
successCallback: null
});
},
penalize(cb) {
let before = this.get('before');
let promise = before ? before() : Ember.RSVP.resolve();
return promise
.then(() => cb())
.then(result => {
this.send('closeModal');
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
})
.catch(popupAjaxError);
}
});

View File

@ -10,7 +10,7 @@ const StaffActionLog = Discourse.Model.extend({
}.property('action_name'),
formattedDetails: function() {
var formatted = "";
let formatted = "";
formatted += this.format('email', 'email');
formatted += this.format('admin.logs.ip_address', 'ip_address');
formatted += this.format('admin.logs.topic_id', 'topic_id');
@ -26,9 +26,13 @@ const StaffActionLog = Discourse.Model.extend({
return formatted;
}.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
format: function(label, propertyName) {
format(label, propertyName) {
if (this.get(propertyName)) {
return ('<b>' + I18n.t(label) + ':</b> ' + escapeExpression(this.get(propertyName)) + '<br/>');
let value = escapeExpression(this.get(propertyName));
if (propertyName === 'post_id') {
value = `<a href data-link-post-id="${value}">${value}</a>`;
}
return `<b>${I18n.t(label)}:</b> ${value}<br/>`;
} else {
return '';
}

View File

@ -52,7 +52,10 @@ export default Ember.Service.extend({
modalClass: `${type}-user-modal`
});
if (opts.post) {
controller.set('post', opts.post);
controller.setProperties({
post: opts.post,
postEdit: opts.post.get('raw')
});
}
return (user.adminUserView ?
@ -62,6 +65,7 @@ export default Ember.Service.extend({
controller.setProperties({
user: loadedUser,
loadingUser: false,
before: opts.before,
successCallback: opts.successCallback
});
});

View File

@ -1,12 +1,12 @@
<div class='admin-backups'>
<div class="admin-controls">
<div class="span15">
<nav>
<ul class="nav nav-pills">
{{nav-item route='admin.backups.index' label='admin.backups.menu.backups'}}
{{nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}}
{{plugin-outlet name="downloader" tagName=""}}
</ul>
</div>
</nav>
<div class="pull-right">
{{#if model.canRollback}}
{{d-button action="rollback"

View File

@ -1,4 +1,4 @@
{{#d-section class="current-badge span13"}}
{{#d-section class="current-badge content-body"}}
<p>{{i18n 'admin.badges.none_selected'}}</p>
<div>

View File

@ -1,4 +1,4 @@
{{#d-section class="current-badge span13"}}
{{#d-section class="current-badge content-body"}}
<form class="form-horizontal">
<div>
<label for="name">{{i18n 'admin.badges.name'}}</label>
@ -144,7 +144,7 @@
{{/d-section}}
{{#if grant_count}}
<div class="span13 current-badge-actions">
<div class="content-body current-badge-actions">
<div>
{{#link-to 'badges.show' this}}{{i18n 'badges.granted' count=grant_count}}{{/link-to}}
</div>

View File

@ -1,6 +1,6 @@
<div class="badges">
<div class='content-list span6'>
<div class='content-list'>
<h3>{{i18n 'admin.badges.title'}}</h3>
<ul>
{{#each model as |badge|}}

View File

@ -1,7 +1,7 @@
<div class='admin-controls'>
<div class='span15'>
<nav>
<ul class="nav nav-pills">
{{yield}}
</ul>
</div>
</nav>
</div>

View File

@ -68,12 +68,6 @@
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
{{#if suspended}}
<div class='suspended-message'>
{{i18n "admin.flags.suspended_for_post"}}
</div>
{{/if}}
<div class='flagged-post-controls'>
{{#if canAct}}
{{admin-agree-flag-dropdown
@ -106,15 +100,6 @@
{{admin-delete-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter")}}
{{#unless suspended}}
{{d-button
class="btn-danger suspend-user"
icon="ban"
label="admin.flags.suspend_user"
title="admin.flags.suspend_user_title"
action=(action "showSuspendModal")}}
{{/unless}}
{{/if}}
{{d-button

View File

@ -0,0 +1,16 @@
<div class='penalty-post-controls'>
<label>
<div class='penalty-post-label'>
{{{i18n 'admin.user.penalty_post_actions'}}}
</div>
</label>
{{combo-box value=postAction content=penaltyActions onSelect=(action "penaltyChanged")}}
</div>
{{#if editing}}
<div class='penalty-post-edit'>
{{textarea
value=postEdit
class="post-editor"}}
</div>
{{/if}}

View File

@ -1,4 +1,4 @@
<div class='content-list span6 color-schemes'>
<div class='content-list color-schemes'>
<h3>{{i18n 'admin.customize.colors.long_title'}}</h3>
<ul>
{{#each model as |scheme|}}

View File

@ -1,5 +1,5 @@
<div class='row'>
<div class='content-list span6'>
<div class='content-list'>
<ul>
{{#each sortedTemplates as |et|}}
<li>

View File

@ -1,5 +1,5 @@
{{#unless editingTheme}}
<div class='content-list span6'>
<div class='content-list'>
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
<ul>
{{#each sortedThemes as |theme|}}

View File

@ -19,7 +19,7 @@
<div class='controls'>
{{text-field value=testEmailAddress placeholderKey="admin.email.test_email_address"}}
</div>
<div class='span10 controls'>
<div class='controls'>
<button class='btn btn-primary' {{action "sendTestEmail"}} disabled={{sendTestEmailDisabled}}>{{i18n 'admin.email.send_test'}}</button>
{{#if sentTestEmail}}<span class='result-message'>{{i18n 'admin.email.sent_test'}}</span>{{/if}}
</div>

View File

@ -6,7 +6,7 @@
<p>{{emoji-uploader done="emojiUploaded"}}</p>
{{#if sortedEmojis}}
<div class="span8">
<div>
<table id="custom_emoji">
<thead>
<tr>

View File

@ -1,6 +1,6 @@
<div class='row groups'>
{{#if sortedGroups}}
<div class='content-list span6'>
<div class='content-list'>
<h3>{{i18n 'admin.groups.edit'}}</h3>
<ul>
{{#each sortedGroups as |group|}}
@ -25,7 +25,7 @@
</div>
{{/if}}
<div class="span13">
<div class="content-body">
{{outlet}}
</div>
</div>

View File

@ -39,7 +39,7 @@
</div>
<div class="clearfix"></div>
<div class='table staff-actions'>
{{#staff-actions}}
<div class="heading-container">
<div class="col heading first staff_user">{{i18n 'admin.logs.staff_actions.staff_user'}}</div>
<div class="col heading action">{{i18n 'admin.logs.action'}}</div>
@ -86,4 +86,4 @@
{{i18n 'search.no_results'}}
{{/each}}
{{/conditional-loading-spinner}}
</div>
{{/staff-actions}}

View File

@ -12,6 +12,12 @@
</div>
{{silence-details reason=reason message=message}}
{{#if post}}
{{penalty-post-action
post=post
postAction=postAction
postEdit=postEdit}}
{{/if}}
{{/conditional-loading-spinner}}

View File

@ -13,6 +13,13 @@
</div>
{{suspension-details reason=reason message=message}}
{{#if post}}
{{penalty-post-action
post=post
postAction=postAction
postEdit=postEdit}}
{{/if}}
{{else}}
<div class='cant-suspend'>
{{i18n "admin.user.cant_suspend"}}

View File

@ -1,9 +1,9 @@
<div class='admin-controls'>
<div class='span15'>
<nav>
<ul class='nav nav-pills'>
<li>{{#link-to 'adminUser' user}}{{d-icon "caret-left"}} &nbsp;{{user.username}}{{/link-to}}</li>
</ul>
</div>
</nav>
</div>
{{#conditional-loading-spinner condition=loading}}

View File

@ -124,7 +124,7 @@
</div>
</div>
<div class='display-row'>
<div class='display-row last-ip'>
<div class='field'>{{i18n 'user.ip_address.title'}}</div>
<div class='value'>{{model.ip_address}}</div>
<div class='controls'>
@ -135,7 +135,7 @@
</div>
</div>
<div class='display-row'>
<div class='display-row registration-ip'>
<div class='field'>{{i18n 'user.registration_ip_address.title'}}</div>
<div class='value'>{{model.registration_ip_address}}</div>
<div class='controls'>

View File

@ -1,10 +1,10 @@
<div class='admin-controls'>
<div class='span15'>
<nav>
<ul class="nav nav-pills">
<li>{{#link-to 'adminUser' model}}{{d-icon "caret-left"}} &nbsp;{{model.username}}{{/link-to}}</li>
<li>{{#link-to 'adminUsersList.show' 'member'}}{{i18n 'admin.user.trust_level_2_users'}}{{/link-to}}</li>
</ul>
</div>
</nav>
</div>
<div class="admin-container tl3-requirements">

View File

@ -1,5 +1,5 @@
<div class='admin-controls'>
<div class='span15'>
<nav>
<ul class="nav nav-pills">
{{nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}}
{{nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}}
@ -11,7 +11,7 @@
{{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}}
{{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
</ul>
</div>
</nav>
<div class="pull-right">
{{#unless siteSettings.enable_sso}}
{{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}

View File

@ -25,6 +25,7 @@
//= require ./discourse/lib/key-value-store
//= require ./discourse/lib/computed
//= require ./discourse/lib/formatter
//= require ./discourse/lib/text-direction
//= require ./discourse/lib/eyeline
//= require ./discourse/lib/show-modal
//= require ./discourse/mixins/scrolling

View File

@ -90,7 +90,7 @@ registerIconRenderer({
if (params.label) { html += " aria-hidden='true'"; }
html += `></${tagName}>`;
if (params.label) {
html += "<span class='sr-only'>" + I18n.t(params.label) + "</span>";
html += `<span class='sr-only'>${params.label}</span>`;
}
return html;
},

View File

@ -0,0 +1,63 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import { PRIVATE_MESSAGE, CREATE_TOPIC, REPLY, EDIT } from "discourse/models/composer";
import { iconHTML } from 'discourse-common/lib/icon-library';
export default Ember.Component.extend({
classNames: ["composer-action-title"],
options: Ember.computed.alias("model.replyOptions"),
action: Ember.computed.alias("model.action"),
isEditing: Ember.computed.equal("action", EDIT),
@computed("options", "action")
actionTitle(opts, action) {
switch (action) {
case PRIVATE_MESSAGE:
return I18n.t("topic.private_message");
case CREATE_TOPIC:
return I18n.t("topic.create_long");
case REPLY:
if (opts.userAvatar && opts.userLink) {
return this._formatReplyToUserPost(opts.userAvatar, opts.userLink);
} else if (opts.topicLink) {
return this._formatReplyToTopic(opts.topicLink);
}
case EDIT:
if (opts.userAvatar && opts.userLink && opts.postLink) {
return this._formatEditUserPost(
opts.userAvatar,
opts.userLink,
opts.postLink,
opts.originalUser
);
}
};
},
_formatEditUserPost(userAvatar, userLink, postLink, originalUser) {
let editTitle = `
<a class="post-link" href="${postLink.href}">${postLink.anchor}</a>
${userAvatar}
<span class="username">${userLink.anchor}</span>
${iconHTML("mail-forward", { class: "reply-to-glyph" })}
`;
if (originalUser) {
editTitle += `
${originalUser.avatar}
<span class="original-username">${originalUser.username}</span>
`;
}
return editTitle.htmlSafe();
},
_formatReplyToTopic(link) {
return `<a class="topic-link" href="${link.href}">${link.anchor}</a>`.htmlSafe();
},
_formatReplyToUserPost(avatar, link) {
const htmlLink = `<a class="user-link" href="${link.href}">${link.anchor}</a>`;
return `${avatar}${htmlLink}`.htmlSafe();
},
});

View File

@ -8,6 +8,7 @@ import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji';
import { emojiUrlFor } from 'discourse/lib/text';
import { getRegister } from 'discourse-common/lib/get-owner';
import { findRawTemplate } from 'discourse/lib/raw-templates';
import { siteDir } from 'discourse/lib/text-direction';
import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities';
import toMarkdown from 'discourse/lib/to-markdown';
import deprecated from 'discourse-common/lib/deprecated';
@ -44,7 +45,8 @@ const isInside = (text, regex) => {
class Toolbar {
constructor(site) {
constructor(opts) {
const { site, siteSettings } = opts;
this.shortcuts = {};
this.groups = [
@ -73,7 +75,14 @@ class Toolbar {
perform: e => e.applySurround('_', '_', 'italic_text')
});
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
if (opts.showLink) {
this.addButton({
id: 'link',
group: 'insertions',
shortcut: 'K',
action: 'showLinkModal'
});
}
this.addButton({
id: 'quote',
@ -107,6 +116,17 @@ class Toolbar {
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
});
if (siteSettings.support_mixed_text_direction) {
this.addButton({
id: 'toggle-direction',
group: 'extras',
icon: 'exchange',
shortcut: 'Shift+6',
title: 'composer.toggle_direction',
perform: e => e.toggleDirection(),
});
}
if (site.mobileView) {
this.groups.push({group: 'mobileExtras', buttons: []});
}
@ -188,6 +208,7 @@ export default Ember.Component.extend({
lastSel: null,
_mouseTrap: null,
emojiPickerIsActive: false,
showLink: true,
@computed('placeholder')
placeholderTranslated(placeholder) {
@ -267,7 +288,9 @@ export default Ember.Component.extend({
@computed
toolbar() {
const toolbar = new Toolbar(this.site);
const toolbar = new Toolbar(
this.getProperties('site', 'siteSettings', 'showLink')
);
_createCallbacks.forEach(cb => cb(toolbar));
this.sendAction('extraButtons', toolbar);
return toolbar;
@ -647,6 +670,14 @@ export default Ember.Component.extend({
return null;
},
_toggleDirection() {
const $textArea = $(".d-editor-input");
let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(),
newDir = currentDir === 'ltr' ? 'rtl' : 'ltr';
$textArea.attr('dir', newDir).focus();
},
paste(e) {
if (!$(".d-editor-input").is(":focus")) {
return;
@ -724,6 +755,7 @@ export default Ember.Component.extend({
addText: text => this._addText(selected, text),
replaceText: text => this._addText({pre: '', post: ''}, text),
getText: () => this.get('value'),
toggleDirection: () => this._toggleDirection(),
};
if (button.sendAction) {

View File

@ -34,13 +34,13 @@ export default Ember.Component.extend({
@computed('message.length')
customMessageLengthClasses(messageLength) {
return (messageLength < Discourse.SiteSettings.min_private_message_post_length) ? "too-short" : "ok";
return (messageLength < Discourse.SiteSettings.min_personal_message_post_length) ? "too-short" : "ok";
},
@computed('message.length')
customMessageLength(messageLength) {
const len = messageLength || 0;
const minLen = Discourse.SiteSettings.min_private_message_post_length;
const minLen = Discourse.SiteSettings.min_personal_message_post_length;
if (len === 0) {
return I18n.t("flagging.custom_message.at_least", { count: minLen });
} else if (len < minLen) {

View File

@ -0,0 +1,6 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
@computed('post.url')
postUrl: Discourse.getURL
});

View File

@ -109,12 +109,15 @@ export default Ember.TextField.extend({
url: Discourse.getURL("/tags/filter/search"),
dataType: 'json',
data: function (term) {
const selectedTags = self.get('tags');
const d = {
q: term,
limit: self.siteSettings.max_tag_search_results,
categoryId: self.get('categoryId'),
selected_tags: self.get('tags')
categoryId: self.get('categoryId')
};
if (selectedTags) {
d.selected_tags = selectedTags.slice(0,100);
}
if (!self.get('everyTag')) {
d.filterForInput = true;
}

View File

@ -1,7 +1,35 @@
import computed from "ember-addons/ember-computed-decorators";
import { siteDir, isRTL, isLTR } from "discourse/lib/text-direction";
export default Ember.TextField.extend({
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'],
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength', 'dir'],
@computed
dir() {
if (this.siteSettings.support_mixed_text_direction) {
let val = this.value;
if (val) {
return isRTL(val) ? 'rtl' : 'ltr';
} else {
return siteDir();
}
}
},
keyUp(event) {
this._super(event);
if (this.siteSettings.support_mixed_text_direction) {
let val = this.value;
if (isRTL(val)) {
this.set('dir', 'rtl');
} else if (isLTR(val)) {
this.set('dir', 'ltr');
} else {
this.set('dir', siteDir());
}
}
},
@computed("placeholderKey")
placeholder(placeholderKey) {

View File

@ -8,12 +8,12 @@ export default Ember.Component.extend({
@computed('topic.isPrivateMessage')
canArchive(isPM) {
return this.siteSettings.enable_private_messages && isPM;
return this.siteSettings.enable_personal_messages && isPM;
},
@computed('topic.isPrivateMessage')
showNotificationsButton(isPM) {
return (!isPM) || this.siteSettings.enable_private_messages;
return (!isPM) || this.siteSettings.enable_personal_messages;
},
@computed('topic.details.can_invite_to')

View File

@ -8,15 +8,10 @@ export default Ember.Component.extend(bufferedRender({
rerenderTriggers: ['topic.archived', 'topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned', 'topic.is_warning'],
click(e) {
if ($(e.target).hasClass('d-icon-thumb-tack')) {
// only pin unpin for now
if (this.get("canAct") && $(e.target).hasClass('d-icon-thumb-tack')) {
const topic = this.get('topic');
// only pin unpin for now
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
}
topic.get('pinned') ? topic.clearPin() : topic.rePin();
}
return false;

View File

@ -16,20 +16,30 @@ export default TextField.extend({
didInsertElement(opts) {
this._super();
const bool = (n => {
const val = this.get(n);
return val === true || val === "true";
});
var self = this,
selected = [],
groups = [],
currentUser = this.currentUser,
includeMentionableGroups = this.get('includeMentionableGroups') === 'true',
includeMessageableGroups = this.get('includeMessageableGroups') === 'true',
includeGroups = this.get('includeGroups') === 'true',
allowedUsers = this.get('allowedUsers') === 'true';
includeMentionableGroups = bool('includeMentionableGroups'),
includeMessageableGroups = bool('includeMessageableGroups'),
includeGroups = bool('includeGroups'),
allowedUsers = bool('allowedUsers'),
excludeCurrentUser = bool('excludeCurrentUser'),
single = bool('single'),
allowAny = bool('allowAny'),
disabled = bool('disabled');
function excludedUsernames() {
// hack works around some issues with allowAny eventing
const usernames = self.get('single') ? [] : selected;
const usernames = single ? [] : selected;
if (currentUser && self.get('excludeCurrentUser')) {
if (currentUser && excludeCurrentUser) {
return usernames.concat([currentUser.get('username')]);
}
return usernames;
@ -37,9 +47,9 @@ export default TextField.extend({
this.$().val(this.get('usernames')).autocomplete({
template: findRawTemplate('user-selector-autocomplete'),
disabled: this.get('disabled'),
single: this.get('single'),
allowAny: this.get('allowAny'),
disabled: disabled,
single: single,
allowAny: allowAny,
updateData: (opts && opts.updateData) ? opts.updateData : false,
dataSource(term) {

View File

@ -80,7 +80,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (selected.get('is_custom_flag')) {
const len = this.get('message.length') || 0;
return len >= Discourse.SiteSettings.min_private_message_post_length &&
return len >= Discourse.SiteSettings.min_personal_message_post_length &&
len <= MAX_MESSAGE_LENGTH;
}
return true;

View File

@ -20,48 +20,54 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
actions: {
submit() {
if (this.get('submitDisabled')) return false;
this.set('disabled', true);
ajax('/session/forgot_password', {
data: { login: this.get('accountEmailOrUsername').trim() },
type: 'POST'
}).then(data => {
const escaped = escapeExpression(this.get('accountEmailOrUsername'));
const isEmail = this.get('accountEmailOrUsername').match(/@/);
let key = 'forgot_password.complete_' + (isEmail ? 'email' : 'username');
let extraClass;
if (data.user_found === true) {
key += '_found';
this.set('accountEmailOrUsername', '');
this.set('offerHelp', I18n.t(key, {email: escaped, username: escaped}));
} else {
if (data.user_found === false) {
key += '_not_found';
extraClass = 'error';
}
this.flash(I18n.t(key, {email: escaped, username: escaped}), extraClass);
}
}).catch(e => {
this.flash(extractError(e), 'error');
}).finally(() => {
setTimeout(() => this.set('disabled', false), 1000);
});
return false;
},
ok() {
this.send('closeModal');
},
help() {
this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true });
}
}
},
resetPassword() {
return this._submit('/session/forgot_password', 'forgot_password.complete');
},
emailLogin() {
return this._submit('/u/email-login', 'email_login.complete');
}
},
_submit(route, translationKey) {
if (this.get('submitDisabled')) return false;
this.set('disabled', true);
ajax(route, {
data: { login: this.get('accountEmailOrUsername').trim() },
type: 'POST'
}).then(data => {
const escaped = escapeExpression(this.get('accountEmailOrUsername'));
const isEmail = this.get('accountEmailOrUsername').match(/@/);
let key = `${translationKey}_${isEmail ? 'email' : 'username'}`;
let extraClass;
if (data.user_found === true) {
key += '_found';
this.set('accountEmailOrUsername', '');
this.set('offerHelp', I18n.t(key, { email: escaped, username: escaped }));
} else {
if (data.user_found === false) {
key += '_not_found';
extraClass = 'error';
}
this.flash(I18n.t(key, { email: escaped, username: escaped }), extraClass);
}
}).catch(e => {
this.flash(extractError(e), 'error');
}).finally(() => {
this.set('disabled', false);
});
return false;
},
});

View File

@ -167,9 +167,9 @@ export default Ember.Controller.extend({
return this.currentUser && this.currentUser.staff && hasResults;
},
@computed('expanded', 'model.grouped_search_result.can_create_topic')
canCreateTopic(expanded, userCanCreateTopic) {
return this.currentUser && userCanCreateTopic && !expanded;
@computed('model.grouped_search_result.can_create_topic')
canCreateTopic(userCanCreateTopic) {
return this.currentUser && userCanCreateTopic;
},
@computed('expanded')

View File

@ -6,6 +6,9 @@ export default Ember.Controller.extend({
@computed('model.is_group_user')
showGroupMessages(isGroupUser) {
if (!this.siteSettings.enable_personal_messages) {
return false;
}
return isGroupUser || (this.currentUser && this.currentUser.admin);
}
});

View File

@ -158,6 +158,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
return Group.findAll({ term: term, ignore_automatic: true });
},
@computed('isPrivateTopic', 'isMessage')
includeMentionableGroups(isPrivateTopic, isMessage) {
return !isPrivateTopic && !isMessage;
},
@computed('isMessage', 'emailOrUsername', 'invitingExistingUserToTopic')
successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) {
if (this.get('hasGroups')) {

View File

@ -57,16 +57,24 @@ export default Ember.Controller.extend(PreferencesTabController, {
},
homeChanged() {
const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
const siteHome = this.siteSettings.top_menu.split("|")[0].split(",")[0];
const userHome = USER_HOMES[this.get('model.user_option.homepage_id')];
setDefaultHomepage(userHome || siteHome);
},
@computed()
userSelectableHome() {
return _.map(USER_HOMES, (name, num) => {
return {name: I18n.t('filters.' + name + '.title'), value: Number(num)};
let homeValues = _.invert(USER_HOMES);
let result = [];
this.siteSettings.top_menu.split('|').forEach(m => {
let id = homeValues[m];
if (id) {
result.push({ name: I18n.t(`filters.${m}.title`), value: Number(id) });
}
});
return result;
},
actions: {

View File

@ -52,7 +52,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
@computed('viewingSelf', 'currentUser.admin')
showPrivateMessages(viewingSelf, isAdmin) {
return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin);
return this.siteSettings.enable_personal_messages && (viewingSelf || isAdmin);
},
@computed('viewingSelf', 'currentUser.staff')

View File

@ -1,11 +1,13 @@
import { htmlHelper } from 'discourse-common/lib/helpers';
import { avatarImg } from 'discourse/lib/utilities';
import { addExtraUserClasses } from 'discourse/helpers/user-avatar';
export default htmlHelper((user, size) => {
if (Ember.isEmpty(user)) {
return "<div class='avatar-placeholder'></div>";
}
const avatarTemplate = Em.get(user, 'avatar_template');
const avatarTemplate = Ember.get(user, 'avatar_template');
return avatarImg({ size, avatarTemplate });
return avatarImg(addExtraUserClasses(user, { size, avatarTemplate }));
});

View File

@ -1,4 +1,5 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import { isRTL } from "discourse/lib/text-direction";
import { iconHTML } from 'discourse-common/lib/icon-library';
var get = Em.get,
@ -38,6 +39,7 @@ export function categoryBadgeHTML(category, opts) {
let color = get(category, 'color');
let html = "";
let parentCat = null;
let categoryDir = "";
if (!opts.hideParent) {
parentCat = Discourse.Category.findById(get(category, 'parent_category_id'));
@ -66,10 +68,14 @@ export function categoryBadgeHTML(category, opts) {
let categoryName = escapeExpression(get(category, 'name'));
if (Discourse.SiteSettings.support_mixed_text_direction) {
categoryDir = isRTL(categoryName) ? 'dir="rtl"' : 'dir="ltr"';
}
if (restricted) {
html += `${iconHTML('lock')}<span>${categoryName}</span>`;
html += `${iconHTML('lock')}<span class="category-name" ${categoryDir}>${categoryName}</span>`;
} else {
html += `<span>${categoryName}</span>`;
html += `<span class="category-name" ${categoryDir}>${categoryName}</span>`;
}
html += "</span>";

View File

@ -0,0 +1,15 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { isRTL } from 'discourse/lib/text-direction';
function setDir(text) {
let content = text ? text : "";
if (content && Discourse.SiteSettings.support_mixed_text_direction) {
let textDir = isRTL(content) ? 'rtl' : 'ltr';
return `<span dir="${textDir}">${content}</span>`;
}
return content;
}
export default registerUnbound('dir-span', function(str) {
return new Handlebars.SafeString(setDir(str));
});

View File

@ -30,7 +30,7 @@ export default htmlHelper((period, options) => {
break;
}
return `${title} <span class='top-date-string'>${dateString}</span>`;
return `<span class="date-section">${title}</span><span class='top-date-string'>${dateString}</span>`;
} else {
return title;
}

View File

@ -1,6 +1,31 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import { avatarImg, formatUsername } from 'discourse/lib/utilities';
let _customAvatarHelpers;
export function registerCustomAvatarHelper(fn) {
_customAvatarHelpers = _customAvatarHelpers || [];
_customAvatarHelpers.push(fn);
}
export function addExtraUserClasses(u, args) {
let extraClasses = classesForUser(u).join(' ');
if (extraClasses && extraClasses.length) {
args.extraClasses = extraClasses;
}
return args;
}
export function classesForUser(u) {
let result = [];
if (_customAvatarHelpers) {
for (let i=0; i<_customAvatarHelpers.length; i++) {
result = result.concat(_customAvatarHelpers[i](u));
}
}
return result;
}
function renderAvatar(user, options) {
options = options || {};

View File

@ -5,7 +5,6 @@ export default {
after: "message-bus",
initialize(container) {
const banner = Em.Object.create(PreloadStore.get("banner")),
site = container.lookup('site:main');

View File

@ -1,13 +1,18 @@
import highlightSyntax from 'discourse/lib/highlight-syntax';
import lightbox from 'discourse/lib/lightbox';
import { setTextDirections } from "discourse/lib/text-direction";
import { withPluginApi } from 'discourse/lib/plugin-api';
export default {
name: "post-decorations",
initialize() {
initialize(container) {
withPluginApi('0.1', api => {
const siteSettings = container.lookup('site-settings:main');
api.decorateCooked(highlightSyntax);
api.decorateCooked(lightbox);
if (siteSettings.support_mixed_text_direction) {
api.decorateCooked(setTextDirections);
}
api.decorateCooked($elem => {
const players = $('audio', $elem);

View File

@ -2,11 +2,27 @@ export default {
name: 'register-service-worker',
initialize() {
const isSecure = (document.location.protocol === 'https:') ||
(location.hostname === "localhost");
window.addEventListener('load', () => {
const isSecured = (document.location.protocol === 'https:') ||
(location.hostname === "localhost");
if (isSecure && ('serviceWorker' in navigator)) {
navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`);
}
const isSupported= isSecured && ('serviceWorker' in navigator);
if (isSupported) {
if (Discourse.ServiceWorkerURL) {
navigator.serviceWorker
.register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`)
.catch(error => {
Ember.Logger.info(`Failed to register Service Worker: ${error}`);
});
} else {
navigator.serviceWorker.getRegistrations().then(registrations => {
for(let registration of registrations) {
registration.unregister();
};
});
}
}
});
}
};

View File

@ -54,9 +54,11 @@ export function throwAjaxError(undoCallback) {
}
export function popupAjaxError(error) {
if (error && error._discourse_displayed) { return; }
bootbox.alert(extractError(error));
error._discourse_displayed = true;
// We re-throw in a catch to not swallow the exception
throw error;
}

View File

@ -71,7 +71,7 @@ export default {
let siteSettings = this.container.lookup('site-settings:main');
// Disable the shortcut if private messages are disabled
if (!siteSettings.enable_private_messages) {
if (!siteSettings.enable_personal_messages) {
delete bindings['g m'];
}

View File

@ -23,9 +23,10 @@ import { addNavItem } from 'discourse/models/nav-item';
import { replaceFormatter } from 'discourse/lib/utilities';
import { modifySelectKit } from "select-kit/mixins/plugin-api";
import { addGTMPageChangedCallback } from 'discourse/lib/page-tracker';
import { registerCustomAvatarHelper } from 'discourse/helpers/user-avatar';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = '0.8.17';
const PLUGIN_API_VERSION = '0.8.18';
class PluginApi {
constructor(version, container) {
@ -368,6 +369,23 @@ class PluginApi {
appEvents.on(name, fn);
}
/**
Registers a function to generate custom avatar CSS classes
for a particular user.
Takes a function that will accept a user as a parameter
and return an array of CSS classes to apply.
```javascript
api.customUserAvatarClasses(user => {
if (Ember.get(user, 'primary_group_name') === 'managers') {
return ['managers'];
}
});
**/
customUserAvatarClasses(fn) {
registerCustomAvatarHelper(fn);
}
/**
* Changes a setting associated with a widget. For example, if

View File

@ -0,0 +1,30 @@
const ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF';
const rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';
const rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']');
const ltrDirCheck = new RegExp('^[^'+rtlChars+']*['+ltrChars+']');
let _siteDir;
export function isRTL(text) {
return rtlDirCheck.test(text);
}
export function isLTR(text) {
return ltrDirCheck.test(text);
}
export function setTextDirections($elem) {
$elem.find('*').each((i, e) => {
let $e = $(e),
textContent = $e.text();
if (textContent) {
isRTL(textContent) ? $e.attr('dir', 'rtl') : $e.attr('dir', 'ltr');
}
});
}
export function siteDir() {
if (!_siteDir) {
_siteDir = $('html').hasClass('rtl') ? 'rtl' : 'ltr';
}
return _siteDir;
}

View File

@ -3,6 +3,7 @@ import parseHTML from 'discourse/helpers/parse-html';
const trimLeft = text => text.replace(/^\s+/,"");
const trimRight = text => text.replace(/\s+$/,"");
const countPipes = text => (text.replace(/\\\|/,"").match(/\|/g) || []).length;
const msoListClasses = ["MsoListParagraphCxSpFirst", "MsoListParagraphCxSpMiddle", "MsoListParagraphCxSpLast"];
class Tag {
constructor(name, prefix = "", suffix = "", inline = false) {
@ -207,7 +208,22 @@ class Tag {
static li() {
return class extends Tag.slice("li", "\n") {
decorate(text) {
const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join("");
let indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join("");
const attrs = this.element.attributes;
if (msoListClasses.includes(attrs.class)) {
try {
const level = parseInt(attrs.style.match(/level./)[0].replace("level", ""));
indent = Array(level).join("\t") + indent;
} finally {
if (attrs.class === "MsoListParagraphCxSpFirst") {
indent = `\n\n${indent}`;
} else if (attrs.class === "MsoListParagraphCxSpLast") {
text = `${text}\n`;
}
}
}
return super.decorate(`${indent}* ${trimLeft(text)}`);
}
};
@ -356,6 +372,13 @@ class Element {
this.parentNames = this.parentNames || [];
this.previous = previous;
this.next = next;
if (this.name === "p") {
if (msoListClasses.includes(this.attributes.class)) {
this.name = "li";
this.parentNames.push("ul");
}
}
}
tag() {
@ -433,7 +456,7 @@ class Element {
}
}
function trimUnwantedSpaces(html) {
function trimUnwanted(html) {
const body = html.match(/<body[^>]*>([\s\S]*?)<\/body>/);
html = body ? body[1] : html;
html = html.replace(/\r|\n|&nbsp;/g, " ");
@ -443,6 +466,8 @@ function trimUnwantedSpaces(html) {
html = html.replace(match[0], match[0].replace(/>\s{2,}</, "> <"));
}
html = html.replace(/<!\[if !?\S*]>[^!]*<!\[endif]>/g, ""); // to support ms word list tags
return html;
}
@ -461,7 +486,7 @@ function putPlaceholders(html) {
match = codeRegEx.exec(origHtml);
}
const elements = parseHTML(trimUnwantedSpaces(html));
const elements = parseHTML(trimUnwanted(html));
return { elements, placeholders };
}

View File

@ -5,6 +5,11 @@ import { defaultHomepage } from 'discourse/lib/utilities';
const rewrites = [];
const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
function redirectTo(url) {
document.location = url;
return true;
}
// We can add links here that have server side responses but not client side.
const SERVER_SIDE_ONLY = [
/^\/assets\//,
@ -17,6 +22,7 @@ const SERVER_SIDE_ONLY = [
/^\/wizard/,
/\.rss$/,
/\.json$/,
/^\/admin\/upgrade$/
];
export function rewritePath(path) {
@ -162,15 +168,23 @@ const DiscourseURL = Ember.Object.extend({
if (Em.isEmpty(path)) { return; }
if (Discourse.get('requiresRefresh')) {
document.location.href = Discourse.getURL(path);
return;
return redirectTo(Discourse.getURL(path));
}
const pathname = path.replace(/(https?\:)?\/\/[^\/]+/, '');
let baseUri = Discourse.BaseUri;
// If we have a baseUri and an absolute URL, make sure the baseUri
// is the same. Otherwise we could be switching forums.
if (baseUri &&
path.indexOf('http') === 0 &&
pathname.indexOf(baseUri) !== 0) {
return redirectTo(path);
}
const serverSide = SERVER_SIDE_ONLY.some(r => {
if (pathname.match(r)) {
document.location = path;
return true;
return redirectTo(path);
}
});
@ -178,8 +192,7 @@ const DiscourseURL = Ember.Object.extend({
// Protocol relative URLs
if (path.indexOf('//') === 0) {
document.location = path;
return;
return redirectTo(path);
}
// Scroll to the same page, different anchor
@ -193,19 +206,19 @@ const DiscourseURL = Ember.Object.extend({
path = path.replace(/(https?\:)?\/\/[^\/]+/, '');
// Rewrite /my/* urls
if (path.indexOf(Discourse.BaseUri + '/my/') === 0) {
let myPath = `${baseUri}/my/`;
if (path.indexOf(myPath) === 0) {
const currentUser = Discourse.User.current();
if (currentUser) {
path = path.replace(Discourse.BaseUri + '/my/', userPath(currentUser.get('username_lower') + "/"));
path = path.replace(myPath, userPath(currentUser.get('username_lower') + "/"));
} else {
document.location.href = "/404";
return;
return redirectTo('/404');
}
}
// handle prefixes
if (path.match(/^\//)) {
let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri);
let rootURL = (baseUri === undefined ? "/" : baseUri);
rootURL = rootURL.replace(/\/$/, '');
path = path.replace(rootURL, '');
}

View File

@ -69,10 +69,11 @@ function organizeResults(r, options) {
if (r.groups) {
r.groups.every(function(g) {
if (results.length > limit && options.term.toLowerCase() !== g.name.toLowerCase()) return false;
if (exclude.indexOf(g.name) === -1) {
groups.push(g);
results.push(g);
if (options.term.toLowerCase() === g.name.toLowerCase() || results.length < limit) {
if (exclude.indexOf(g.name) === -1) {
groups.push(g);
results.push(g);
}
}
return true;
});

View File

@ -369,7 +369,7 @@ export function displayErrorForUpload(data) {
if (data.jqXHR.responseJSON.message) {
bootbox.alert(data.jqXHR.responseJSON.message);
} else {
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"));
}
return;
}

View File

@ -7,8 +7,8 @@ export default Ember.Mixin.create({
this.controllerFor('composer').open({
categoryId: controller.get('category.id'),
action: Composer.CREATE_TOPIC,
draftKey: controller.get('model.draft_key'),
draftSequence: controller.get('model.draft_sequence')
draftKey: controller.get('model.draft_key') || Composer.CREATE_TOPIC,
draftSequence: controller.get('model.draft_sequence') || 0
});
},

View File

@ -1,4 +1,3 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
import RestModel from 'discourse/models/rest';
import Topic from 'discourse/models/topic';
import { throwAjaxError } from 'discourse/lib/ajax-error';
@ -6,45 +5,45 @@ import Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft';
import computed from 'ember-addons/ember-computed-decorators';
import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities';
import { emojiUnescape } from 'discourse/lib/text';
// The actions the composer can take
export const
CREATE_TOPIC = 'createTopic',
PRIVATE_MESSAGE = 'privateMessage',
NEW_PRIVATE_MESSAGE_KEY = 'new_private_message',
REPLY = 'reply',
EDIT = 'edit',
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message";
const CLOSED = 'closed',
SAVING = 'saving',
OPEN = 'open',
DRAFT = 'draft',
SAVING = 'saving',
OPEN = 'open',
DRAFT = 'draft',
// The actions the composer can take
CREATE_TOPIC = 'createTopic',
PRIVATE_MESSAGE = 'privateMessage',
NEW_PRIVATE_MESSAGE_KEY = 'new_private_message',
REPLY = 'reply',
EDIT = 'edit',
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message",
// When creating, these fields are moved into the post model from the composer model
_create_serializer = {
raw: 'reply',
title: 'title',
unlist_topic: 'unlistTopic',
category: 'categoryId',
topic_id: 'topic.id',
is_warning: 'isWarning',
whisper: 'whisper',
archetype: 'archetypeId',
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime',
tags: 'tags',
featured_link: 'featuredLink'
},
// When creating, these fields are moved into the post model from the composer model
_create_serializer = {
raw: 'reply',
title: 'title',
unlist_topic: 'unlistTopic',
category: 'categoryId',
topic_id: 'topic.id',
is_warning: 'isWarning',
whisper: 'whisper',
archetype: 'archetypeId',
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime',
tags: 'tags',
featured_link: 'featuredLink'
},
_edit_topic_serializer = {
title: 'topic.title',
categoryId: 'topic.category.id',
tags: 'topic.tags',
featuredLink: 'topic.featured_link'
};
_edit_topic_serializer = {
title: 'topic.title',
categoryId: 'topic.category.id',
tags: 'topic.tags',
featuredLink: 'topic.featured_link'
};
const _saveLabels = {};
_saveLabels[EDIT] = 'composer.save_edit';
@ -167,52 +166,55 @@ const Composer = RestModel.extend({
return this.get('canEditTopicFeaturedLink') ? 'composer.title_or_link_placeholder' : 'composer.title_placeholder';
},
// Determine the appropriate title for this action
actionTitle: function() {
const topic = this.get('topic');
@computed("action", "post", "topic", "topic.title")
replyOptions(action, post, topic, topicTitle) {
let options = {
userLink: null,
topicLink: null,
postLink: null,
userAvatar: null,
originalUser: null
};
let postLink, topicLink, usernameLink;
if (topic) {
const postNumber = this.get('post.post_number');
postLink = "<a href='" + (topic.get('url')) + "/" + postNumber + "'>" +
I18n.t("post.post_number", { number: postNumber }) + "</a>";
let title = topic.get('fancy_title') || escapeExpression(topic.get('title'));
topicLink = "<a href='" + (topic.get('url')) + "'> " + title + "</a>";
usernameLink = "<a href='" + (topic.get('url')) + "/" + postNumber + "'>" + this.get('post.username') + "</a>";
options.topicLink = {
href: topic.get("url"),
anchor: topic.get("fancy_title") || escapeExpression(topicTitle)
};
}
let postDescription;
const post = this.get('post');
if (post) {
postDescription = I18n.t('post.' + this.get('action'), {
link: postLink,
replyAvatar: tinyAvatar(post.get('avatar_template')),
username: this.get('post.username'),
usernameLink
});
options.label = I18n.t(`post.${action}`);
options.userAvatar = tinyAvatar(post.get("avatar_template"));
if (!this.site.mobileView) {
const replyUsername = post.get('reply_to_user.username');
const replyAvatarTemplate = post.get('reply_to_user.avatar_template');
if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
postDescription += ` ${iconHTML('mail-forward', { class: 'reply-to-glyph' })} ` + tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
const originalUserName = post.get('reply_to_user.username');
const originalUserAvatar = post.get('reply_to_user.avatar_template');
if (originalUserName && originalUserAvatar && action === EDIT) {
options.originalUser = {
username: originalUserName,
avatar: tinyAvatar(originalUserAvatar)
};
}
}
}
switch (this.get('action')) {
case PRIVATE_MESSAGE: return I18n.t('topic.private_message');
case CREATE_TOPIC: return I18n.t('topic.create_long');
case REPLY:
case EDIT:
if (postDescription) return postDescription;
if (topic) return emojiUnescape(I18n.t('post.reply_topic', { link: topicLink }));
if (topic && post) {
const postNumber = post.get("post_number");
options.postLink = {
href: `${topic.get("url")}/${postNumber}`,
anchor: I18n.t("post.post_number", { number: postNumber })
};
options.userLink = {
href: `${topic.get("url")}/${postNumber}`,
anchor: post.get("username")
};
}
}.property('action', 'post', 'topic', 'topic.title'),
return options;
},
// whether to disable the post button
cantSubmitPost: function() {
@ -302,7 +304,7 @@ const Composer = RestModel.extend({
@computed('privateMessage')
minimumTitleLength(privateMessage) {
if (privateMessage) {
return this.siteSettings.min_private_message_title_length;
return this.siteSettings.min_personal_message_title_length;
} else {
return this.siteSettings.min_topic_title_length;
}
@ -325,7 +327,7 @@ const Composer = RestModel.extend({
if (pmWithNonHumanUser) {
return 1;
} else if (privateMessage) {
return this.siteSettings.min_private_message_post_length;
return this.siteSettings.min_personal_message_post_length;
} else if (topicFirstPost) {
// first post (topic body)
return this.siteSettings.min_first_post_length;

View File

@ -168,7 +168,8 @@ const Post = RestModel.extend({
this.setProperties({
deleted_at: new Date(),
deleted_by: deletedBy,
can_delete: false
can_delete: false,
can_recover: true
});
} else {
promise = cookAsync(I18n.t("post.deleted_by_author", {count: Discourse.SiteSettings.delete_removed_posts_after})).then(cooked => {

View File

@ -3,6 +3,7 @@ import { flushMap } from 'discourse/models/store';
import RestModel from 'discourse/models/rest';
import { propertyEqual } from 'discourse/lib/computed';
import { longDate } from 'discourse/lib/formatter';
import { isRTL } from 'discourse/lib/text-direction';
import computed from 'ember-addons/ember-computed-decorators';
import ActionSummary from 'discourse/models/action-summary';
import { popupAjaxError } from 'discourse/lib/ajax-error';
@ -58,7 +59,13 @@ const Topic = RestModel.extend({
@computed('fancy_title')
fancyTitle(title) {
return censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words'));
let fancyTitle = censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words'));
if (Discourse.SiteSettings.support_mixed_text_direction) {
let titleDir = isRTL(title) ? 'rtl' : 'ltr';
return `<span dir="${titleDir}">${fancyTitle}</span>`;
}
return fancyTitle;
},
// returns createdAt if there's no bumped date

View File

@ -30,9 +30,20 @@ export default Discourse.Route.extend({
afterModel(model, transition) {
const username = transition.queryParams && transition.queryParams.username;
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
const userBadgesGrant = UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
this.userBadgesGrant = userBadges;
});
const userBadgesAll = UserBadge.findByUsername(username).then(userBadges => {
this.userBadgesAll = userBadges;
});
const promises = {
userBadgesGrant,
userBadgesAll,
};
return Ember.RSVP.hash(promises);
},
titleToken() {

View File

@ -1,11 +1,19 @@
import Composer from 'discourse/models/composer';
const DiscourseRoute = Ember.Route.extend({
showFooter: false,
// Set to true to refresh a model without a transition if a query param
// changes
resfreshQueryWithoutTransition: false,
activate() {
this._super();
if (this.get('showFooter')) {
this.controllerFor('application').set('showFooter', true);
}
},
refresh() {
if (!this.refreshQueryWithoutTransition) { return this._super(); }

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model: function() {
return this.modelFor('user');
},

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
setupController(controller, user) {
controller.reset();
controller.setProperties({

View File

@ -2,6 +2,8 @@ import UserBadge from 'discourse/models/user-badge';
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model: function() {
return UserBadge.findByUsername(this.modelFor('user').get('username'));
},

View File

@ -2,6 +2,8 @@ import UserBadge from 'discourse/models/user-badge';
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model: function() {
return UserBadge.findByUsername(this.modelFor('user').get('username'));
},

View File

@ -0,0 +1,5 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true
});

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model: function() {
return this.modelFor('user');
},

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
redirect() {
this.transitionTo('preferences.account');
}

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
setupController(controller, user) {
controller.setProperties({
model: user

View File

@ -0,0 +1,5 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true
});

View File

@ -1,6 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model: function() {
return this.modelFor('user');
},

View File

@ -3,6 +3,7 @@ import showModal from 'discourse/lib/show-modal';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default RestrictedUserRoute.extend({
model() {
return this.modelFor('user');
},

View File

@ -1,4 +1,6 @@
export default Discourse.Route.extend({
showFooter: true,
model() {
return this.modelFor("user").summary();
},

View File

@ -16,7 +16,7 @@
<div>
{{category-title-link category=c}}
<div class="category-description">
{{{c.description_excerpt}}}
{{{dir-span c.description_excerpt}}}
</div>
<div class="clearfix"></div>
</div>

View File

@ -3,7 +3,7 @@
{{d-icon 'lock'}}
{{/if}}
<span class="category-name">{{category.name}}</span>
<span class="category-name">{{dir-span category.name}}</span>
{{#if category.uploaded_logo.url}}
<div>{{cdn-img src=category.uploaded_logo.url class="category-logo"}}</div>

View File

@ -0,0 +1,13 @@
{{#if isEditing}}
{{d-icon "pencil"}}
{{else}}
{{composer-actions
composerModel=model
options=model.replyOptions
canWhisper=canWhisper
action=model.action}}
{{/if}}
<span class="action-title">
{{actionTitle}}
</span>

View File

@ -11,6 +11,7 @@
validation=validation
loading=composer.loading
forcePreview=forcePreview
showLink=currentUser.can_post_link
composerEvents=true
onExpandPopupMenuOptions="onExpandPopupMenuOptions"
onPopupMenuAction=onPopupMenuAction

View File

@ -3,7 +3,6 @@
onChangeCallback='triggerResize'
id="private-message-users"
includeMessageableGroups='true'
class="span8"
placeholderKey="composer.users_placeholder"
tabindex="1"
usernames=usernames

View File

@ -7,7 +7,7 @@
<div class='group-post-info'>
<div class="group-post-topic">
<div class='group-post-title'>
<a href={{post.url}}>{{{post.topic.fancyTitle}}}</a>
<a href={{postUrl}}>{{{post.topic.fancyTitle}}}</a>
</div>
<div class="group-post-category">{{category-link post.category}}</div>
{{#if post.user.name}}

View File

@ -103,7 +103,7 @@
{{#if hasLocationOrWebsite}}
<div class="location-and-website">
{{#if user.location}}
<span class='location'>{{d-icon "map-marker"}} {{user.location}}</span>
<span class='location'>{{d-icon "map-marker"}} <span>{{user.location}}</span></span>
{{/if}}
{{#if user.website_name}}

View File

@ -16,7 +16,7 @@
{{plugin-outlet name="composer-open" args=(hash model=model)}}
<div class='reply-to'>
<div class="reply-details">
{{{model.actionTitle}}}
{{composer-action-title model=model canWhisper=canWhisper}}
{{#unless site.mobileView}}
{{#if whisperOrUnlistTopicText}}
@ -65,7 +65,7 @@
</div>
{{/if}}
{{#if canEditTags}}
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId}}
{{/if}}

View File

@ -1,6 +1,6 @@
{{#if topic.hasExcerpt}}
<div class="topic-excerpt">
{{{topic.escapedExcerpt}}}
{{{dir-span topic.escapedExcerpt}}}
{{#if topic.excerptTruncated}}
<a href="{{topic.url}}">{{i18n 'read_more'}}</a>
{{/if}}

View File

@ -9,10 +9,16 @@
{{/d-modal-body}}
<div class="modal-footer">
{{#unless offerHelp}}
{{d-button action="submit"
label="forgot_password.reset"
disabled=submitDisabled
class="btn-primary"}}
{{d-button action="resetPassword"
label="forgot_password.reset"
disabled=submitDisabled
class="btn-primary"}}
{{#if siteSettings.enable_local_logins_via_email}}
{{d-button action="emailLogin"
label="email_login.label"
disabled=submitDisabled
class="email-login"}}
{{/if}}
{{else}}
{{d-button class="btn-large btn-primary"
label="forgot_password.button_ok"

View File

@ -14,26 +14,16 @@
{{else}}
<label>{{inviteInstructions}}</label>
{{#if allowExistingMembers}}
{{#if isPrivateTopic}}
{{user-selector single="true"
allowAny=true
excludeCurrentUser="true"
usernames=emailOrUsername
allowedUsers="true"
topicId=topicId
placeholderKey=placeholderKey
autocomplete="off"}}
{{else}}
{{user-selector
single="true"
allowAny=true
excludeCurrentUser="true"
includeMentionableGroups="true"
hasGroups=hasGroups
usernames=emailOrUsername
placeholderKey=placeholderKey
autocomplete="off"}}
{{/if}}
{{user-selector
single=true
allowAny=true
excludeCurrentUser=true
includeMentionableGroups=includeMentionableGroups
includeMessageableGroups=isMessage
hasGroups=hasGroups
usernames=emailOrUsername
placeholderKey=placeholderKey
autocomplete="off"}}
{{else}}
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
{{/if}}

View File

@ -1,6 +1,6 @@
{{#d-modal-body id="keyboard-shortcuts-help"}}
<div class="row">
<div class="span6">
<div>
<h4>{{i18n 'keyboard_shortcuts_help.jump_to.title'}}</h4>
<ul>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.home'}}}</li>
@ -11,7 +11,7 @@
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.top'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.bookmarks'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.profile'}}}</li>
{{#if siteSettings.enable_private_messages}}
{{#if siteSettings.enable_personal_messages}}
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.messages'}}}</li>
{{/if}}
</ul>
@ -24,7 +24,7 @@
<li>{{{i18n 'keyboard_shortcuts_help.navigation.next_prev'}}}</li>
</ul>
</div>
<div class="span6">
<div>
<h4>{{i18n 'keyboard_shortcuts_help.application.title'}}</h4>
<ul>
<li>{{{i18n 'keyboard_shortcuts_help.application.hamburger_menu'}}}</li>
@ -46,7 +46,7 @@
<li>{{{i18n 'keyboard_shortcuts_help.actions.quote_post'}}}</li>
</ul>
</div>
<div class="span6">
<div>
<h4>{{i18n 'keyboard_shortcuts_help.actions.title'}}</h4>
<ul>
<li>{{{i18n 'keyboard_shortcuts_help.actions.bookmark_topic'}}}</li>

View File

@ -6,24 +6,14 @@
<div>
<table>
<tr>
<td>
<label for='login-account-name'>{{i18n 'login.username'}}&nbsp;</label>
</td>
<td>
{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus"}}
</td>
<td><label for='login-account-name'>{{i18n 'login.username'}}</label></td>
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus"}}</td>
<td></td>
</tr>
<tr>
<td>
<label for='login-account-password'>{{i18n 'login.password'}}&nbsp;</label>
</td>
<td>
{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn}} &nbsp;
</td>
<td>
<a id="forgot-password-link" {{action "forgotPassword"}}>{{i18n 'forgot_password.action'}}</a>
</td>
<td><label for='login-account-password'>{{i18n 'login.password'}}</label></td>
<td>{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn}}</td>
<td><a id="forgot-password-link" {{action "forgotPassword"}}>{{i18n 'forgot_password.action'}}</a></td>
</tr>
<tr>
<td></td>
@ -40,14 +30,11 @@
<div class="modal-footer">
{{#if canLoginLocal}}
<button class="btn btn-large btn-primary"
disabled={{loginDisabled}}
{{action "login"}}>
{{d-icon "unlock"}} &nbsp;{{loginButtonText}}
<button form="login-form" type="submit" class="btn btn-large btn-primary" disabled={{loginDisabled}} {{action "login"}}>
{{d-icon "unlock"}}&nbsp;{{loginButtonText}}
</button>
{{#if showSignupLink}}
&nbsp;
<button class="btn btn-large" id="new-account-link" {{action "createAccount"}}>
{{i18n 'create_account.title'}}
</button>
@ -55,7 +42,7 @@
{{/if}}
{{#if authenticate}}
&nbsp; {{i18n 'login.authenticating'}}
&nbsp;{{i18n 'login.authenticating'}}
{{/if}}
{{conditional-loading-spinner condition=showSpinner size="small"}}

View File

@ -5,7 +5,7 @@
{{#if category.uploaded_logo.url}}
{{cdn-img src=category.uploaded_logo.url class="category-logo"}}
{{#if category.description}}
<p>{{{category.description}}}</p>
<p>{{{dir-span category.description}}}</p>
{{/if}}
{{/if}}
</section>

View File

@ -26,15 +26,17 @@
<div class="instructions">{{i18n 'user.desktop_notifications.each_browser_note'}}</div>
</div>
<div class="control-group private-messages">
<label class="control-label">{{i18n 'user.private_messages'}}</label>
{{#if siteSettings.enable_personal_messages}}
<div class="control-group private-messages">
<label class="control-label">{{i18n 'user.private_messages'}}</label>
<div class="controls">
{{preference-checkbox
labelKey="user.allow_private_messages"
checked=model.user_option.allow_private_messages}}
<div class="controls">
{{preference-checkbox
labelKey="user.allow_private_messages"
checked=model.user_option.allow_private_messages}}
</div>
</div>
</div>
{{/if}}
<div class="control-group muting">
<label class="control-label">{{i18n 'user.users'}}</label>

View File

@ -42,7 +42,7 @@
{{else}}
<h1>
{{#unless model.is_warning}}
{{#if siteSettings.enable_private_messages}}
{{#if siteSettings.enable_personal_messages}}
<a href={{pmPath}}>
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
</a>

View File

@ -5,13 +5,13 @@
<h2>{{i18n 'user.invited.title'}}</h2>
{{#if model.can_see_invite_details}}
<div class='user-invite-controls'>
<div class='span15'>
<div class='admin-controls'>
<nav>
<ul class="nav nav-pills">
{{nav-item route='userInvited.show' routeParam='pending' i18nLabel=pendingLabel}}
{{nav-item route='userInvited.show' routeParam='redeemed' i18nLabel=redeemedLabel}}
</ul>
</div>
</nav>
<div class="pull-right">
{{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}}

View File

@ -39,7 +39,16 @@
<div class='profile-image'></div>
<div class='details'>
<div class='primary'>
{{bound-avatar model "huge"}}
<div class='user-profile-avatar'>
{{bound-avatar model "huge"}}
{{#if model.primary_group_name}}
{{avatar-flair
flairURL=model.primary_group_flair_url
flairBgColor=model.primary_group_flair_bg_color
flairColor=model.primary_group_flair_color
groupName=model.primary_group_name}}
{{/if}}
</div>
<section class='controls'>
<ul>
{{#if model.can_send_private_message_to_user}}
@ -73,7 +82,7 @@
<h3>{{model.title}}</h3>
{{/if}}
{{plugin-outlet name="user-post-names" args=(hash model=model)}}
<h3>
<h3 class="location-and-website">
{{#if model.location}}{{d-icon "map-marker"}} {{model.location}}{{/if}}
{{#if model.website_name}}
{{d-icon "globe"}}

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