Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2015-11-02 13:24:42 +11:00
commit fbb6eba252
222 changed files with 2726 additions and 1141 deletions

View File

@ -6,11 +6,6 @@ export default Ember.Component.extend({
@computed('field')
inputId(field) { return field.dasherize(); },
@computed('placeholder')
placeholderValue(placeholder) {
return placeholder ? I18n.t(placeholder) : null;
},
@computed('field')
translationKey(field) { return `admin.embedding.${field}`; },
@ -21,7 +16,7 @@ export default Ember.Component.extend({
checked: {
get(value) { return !!value; },
set(value) {
this.set('value', value);
this.set('value', value);
return value;
}
}

View File

@ -5,7 +5,7 @@ export default Ember.Controller.extend({
const model = this.get('model');
this.set('loading', true);
Discourse.EmailPreview.findDigest(this.get('lastSeen')).then(email => {
Discourse.EmailPreview.findDigest(this.get('lastSeen'), this.get('username')).then(email => {
model.setProperties(email.getProperties('html_content', 'text_content'));
this.set('loading', false);
});

View File

@ -0,0 +1,34 @@
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
users: null,
groupId: null,
saving: false,
@computed('saving', 'users', 'groupId')
buttonDisabled(saving, users, groupId) {
return saving || !groupId || !users || !users.length;
},
actions: {
addToGroup() {
if (this.get('saving')) { return; }
const users = this.get('users').split("\n")
.uniq()
.reject(x => x.length === 0);
this.set('saving', true);
Discourse.ajax('/admin/groups/bulk', {
data: { users, group_id: this.get('groupId') },
method: 'PUT'
}).then(() => {
this.transitionToRoute('adminGroups.bulkComplete');
}).catch(popupAjaxError).finally(() => {
this.set('saving', false);
});
}
}
});

View File

@ -288,10 +288,7 @@ const AdminUser = Discourse.User.extend({
data: { username: this.get('username') }
}).then(function() {
bootbox.alert( I18n.t('admin.user.activation_email_sent') );
}).catch(function(e) {
var error = I18n.t('admin.user.send_activation_email_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
}).catch(popupAjaxError);
},
anonymizeForbidden: Em.computed.not("can_be_anonymized"),

View File

@ -9,18 +9,20 @@
Discourse.EmailPreview = Discourse.Model.extend({});
Discourse.EmailPreview.reopenClass({
findDigest: function(lastSeenAt) {
findDigest: function(lastSeenAt, username) {
if (Em.isEmpty(lastSeenAt)) {
lastSeenAt = moment().subtract(7, 'days').format('YYYY-MM-DD');
}
if (Em.isEmpty(username)) {
username = Discourse.User.current().username;
}
return Discourse.ajax("/admin/email/preview-digest.json", {
data: {last_seen_at: lastSeenAt}
data: { last_seen_at: lastSeenAt, username: username }
}).then(function (result) {
return Discourse.EmailPreview.create(result);
});
}
});

View File

@ -0,0 +1,13 @@
import Group from 'discourse/models/group';
export default Ember.Route.extend({
model() {
return Group.findAll().then(groups => {
return groups.filter(g => !g.get('automatic'));
});
},
setupController(controller, groups) {
controller.setProperties({ groups, groupId: null, users: null });
}
});

View File

@ -49,6 +49,8 @@ export default {
});
this.resource('adminGroups', { path: '/groups' }, function() {
this.route('bulk');
this.route('bulkComplete', { path: 'bulk-complete' });
this.resource('adminGroupsType', { path: '/:type' }, function() {
this.resource('adminGroup', { path: '/:name' });
});

View File

@ -5,7 +5,7 @@
</label>
{{else}}
<label for={{inputId}}>{{i18n translationKey}}</label>
{{input value=value id=inputId placeholder=placeholderValue}}
{{input value=value id=inputId placeholder=placeholder}}
{{/if}}
<div class='clearfix'></div>

View File

@ -4,6 +4,8 @@
<div class='span7 controls'>
<label for='last-seen'>{{i18n 'admin.email.last_seen_user'}}</label>
{{input type="date" value=lastSeen id="last-seen"}}
<label>{{i18n 'admin.email.user'}}:</label>
{{user-selector single="true" usernames=username}}
<button class='btn' {{action "refresh"}}>{{i18n 'admin.email.refresh'}}</button>
<div class="toggle">
<label>{{i18n 'admin.email.format'}}</label>

View File

@ -48,11 +48,11 @@
{{embedding-setting field="embed_whitelist_selector"
value=embedding.embed_whitelist_selector
placeholder="admin.embedding.whitelist_example"}}
placeholder="article, #story, .post"}}
{{embedding-setting field="embed_blacklist_selector"
value=embedding.embed_blacklist_selector
placeholder="admin.embedding.blacklist_example"}}
placeholder=".ad-unit, header"}}
</div>
<div class='embedding-secondary'>

View File

@ -0,0 +1 @@
<p>{{i18n "admin.groups.bulk_complete"}}</p>

View File

@ -0,0 +1,19 @@
<div class='groups-bulk'>
<p>{{i18n "admin.groups.bulk_paste"}}</p>
<div class='control'>
{{textarea value=users class="paste-users"}}
</div>
<div class='control'>
{{combo-box content=groups valueAttribute="id" value=groupId none="admin.groups.bulk_select"}}
</div>
<div class='control'>
{{d-button disabled=buttonDisabled
class="btn-primary"
action="addToGroup"
icon="plus"
label="admin.groups.bulk"}}
</div>
</div>

View File

@ -1,6 +1,7 @@
{{#admin-nav}}
{{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
{{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
{{nav-item route='adminGroups.bulk' label='admin.groups.bulk'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -2,7 +2,7 @@
<p class='description'>{{model.description}}</p>
{{#if model.markdown}}
{{pagedown-editor value=model.value}}
{{d-editor value=model.value}}
{{/if}}
{{#if model.plainText}}
{{textarea value=model.value class="plain"}}

View File

@ -14,7 +14,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
if (!url) return url;
// if it's a non relative URL, return it.
if (!/^\/[^\/]/.test(url)) return url;
if (url !== '/' && !/^\/[^\/]/.test(url)) return url;
var u = Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri;

View File

@ -0,0 +1,52 @@
import { observes, on } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNameBindings: [':d-editor-modal', 'hidden'],
@observes('hidden')
_hiddenChanged() {
if (!this.get('hidden')) {
Ember.run.scheduleOnce('afterRender', () => {
const $modal = this.$();
const $parent = this.$().closest('.d-editor');
const w = $parent.width();
const h = $parent.height();
$modal.css({ left: (w / 2) - ($modal.outerWidth() / 2) });
parent.$('.d-editor-overlay').removeClass('hidden').css({ width: w, height: h});
this.$('input').focus();
});
} else {
parent.$('.d-editor-overlay').addClass('hidden');
}
},
@on('didInsertElement')
_listenKeys() {
this.$().on('keydown.d-modal', key => {
if (this.get('hidden')) { return; }
if (key.keyCode === 27) {
this.send('cancel');
}
if (key.keyCode === 13) {
this.send('ok');
}
});
},
@on('willDestroyElement')
_stopListening() {
this.$().off('keydown.d-modal');
},
actions: {
ok() {
this.set('hidden', true);
this.sendAction('okAction');
},
cancel() {
this.set('hidden', true);
}
}
});

View File

@ -0,0 +1,263 @@
import loadScript from 'discourse/lib/load-script';
import { default as property, on } from 'ember-addons/ember-computed-decorators';
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
// Our head can be a static string or a function that returns a string
// based on input (like for numbered lists).
function getHead(head, prev) {
if (typeof head === "string") {
return [head, head.length];
} else {
return getHead(head(prev));
}
}
export default Ember.Component.extend({
classNames: ['d-editor'],
ready: false,
insertLinkHidden: true,
link: '',
lastSel: null,
@on('didInsertElement')
_loadSanitizer() {
this._applyEmojiAutocomplete();
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
},
@property('ready', 'value')
preview(ready, value) {
if (!ready) { return; }
const text = Discourse.Dialect.cook(value || "", {sanitize: true});
return text ? text : "";
},
_applyEmojiAutocomplete() {
if (!this.siteSettings.enable_emoji) { return; }
const container = this.container;
const template = container.lookup('template:emoji-selector-autocomplete.raw');
const self = this;
this.$('.d-editor-input').autocomplete({
template: template,
key: ":",
transformComplete(v) {
if (v.code) {
return `${v.code}:`;
} else {
showSelector({
appendTo: self.$(),
container,
onSelect: title => self._addText(`${title}:`)
});
return "";
}
},
dataSource(term) {
return new Ember.RSVP.Promise(resolve => {
const full = `:${term}`;
term = term.toLowerCase();
if (term === "") {
return resolve(["smile", "smiley", "wink", "sunny", "blush"]);
}
if (Discourse.Emoji.translations[full]) {
return resolve([Discourse.Emoji.translations[full]]);
}
const options = Discourse.Emoji.search(term, {maxResults: 5});
return resolve(options);
}).then(list => list.map(code => {
return {code, src: Discourse.Emoji.urlFor(code)};
})).then(list => {
if (list.length) {
list.push({ label: I18n.t("composer.more_emoji") });
}
return list;
});
}
});
},
_getSelected() {
if (!this.get('ready')) { return; }
const textarea = this.$('textarea.d-editor-input')[0];
let start = textarea.selectionStart;
let end = textarea.selectionEnd;
if (start === end) {
start = end = textarea.value.length;
}
const value = textarea.value.substring(start, end);
const pre = textarea.value.slice(0, start);
const post = textarea.value.slice(end);
return { start, end, value, pre, post };
},
_selectText(from, length) {
Ember.run.scheduleOnce('afterRender', () => {
const textarea = this.$('textarea.d-editor-input')[0];
textarea.focus();
textarea.selectionStart = from;
textarea.selectionEnd = textarea.selectionStart + length;
});
},
_applySurround(head, tail, exampleKey) {
const sel = this._getSelected();
const pre = sel.pre;
const post = sel.post;
const tlen = tail.length;
if (sel.start === sel.end) {
if (tlen === 0) { return; }
const [hval, hlen] = getHead(head);
const example = I18n.t(`composer.${exampleKey}`);
this.set('value', `${pre}${hval}${example}${tail}${post}`);
this._selectText(pre.length + hlen, example.length);
} else {
const lines = sel.value.split("\n");
let [hval, hlen] = getHead(head);
if (lines.length === 1 && pre.slice(-tlen) === tail && post.slice(0, hlen) === hval) {
this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`);
this._selectText(sel.start - hlen, sel.value.length);
} else {
const contents = lines.map(l => {
if (l.length === 0) { return l; }
if (l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail) {
if (tlen === 0) {
const result = l.slice(hlen);
[hval, hlen] = getHead(head, hval);
return result;
} else if (l.slice(-tlen) === tail) {
const result = l.slice(hlen, -tlen);
[hval, hlen] = getHead(head, hval);
return result;
}
}
const result = `${hval}${l}${tail}`;
[hval, hlen] = getHead(head, hval);
return result;
}).join("\n");
this.set('value', `${pre}${contents}${post}`);
if (lines.length === 1 && tlen > 0) {
this._selectText(sel.start + hlen, contents.length - hlen - hlen);
} else {
this._selectText(sel.start, contents.length);
}
}
}
},
_applyList(head, exampleKey) {
const sel = this._getSelected();
if (sel.value.indexOf("\n") !== -1) {
this._applySurround(head, '', exampleKey);
} else {
const [hval, hlen] = getHead(head);
if (sel.start === sel.end) {
sel.value = I18n.t(`composer.${exampleKey}`);
}
const trimmedPre = sel.pre.trim();
const number = (sel.value.indexOf(hval) === 0) ? sel.value.slice(hlen) : `${hval}${sel.value}`;
const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : "";
const trimmedPost = sel.post.trim();
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
this.set('value', `${preLines}${number}${post}`);
this._selectText(preLines.length, number.length);
}
},
_addText(text, sel) {
sel = sel || this._getSelected();
const insert = `${sel.pre}${text}`;
this.set('value', `${insert}${sel.post}`);
this._selectText(insert.length, 0);
},
actions: {
bold() {
this._applySurround('**', '**', 'bold_text');
},
italic() {
this._applySurround('*', '*', 'italic_text');
},
showLinkModal() {
this._lastSel = this._getSelected();
this.set('insertLinkHidden', false);
},
insertLink() {
const link = this.get('link');
if (Ember.isEmpty(link)) { return; }
const m = / "([^"]+)"/.exec(link);
if (m && m.length === 2) {
const description = m[1];
const remaining = link.replace(m[0], '');
this._addText(`[${description}](${remaining})`, this._lastSel);
} else {
this._addText(`[${link}](${link})`, this._lastSel);
}
this.set('link', '');
},
code() {
const sel = this._getSelected();
if (sel.value.indexOf("\n") !== -1) {
this._applySurround(' ', '', 'code_text');
} else {
this._applySurround('`', '`', 'code_text');
}
},
quote() {
this._applySurround('> ', "", 'code_text');
},
bullet() {
this._applyList('* ', 'list_item');
},
list() {
this._applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item');
},
heading() {
this._applyList('## ', 'heading_text');
},
rule() {
this._addText("\n\n----------\n");
},
emoji() {
showSelector({
appendTo: this.$(),
container: this.container,
onSelect: title => this._addText(`:${title}:`)
});
}
}
});

View File

@ -56,6 +56,13 @@ export default Ember.Component.extend({
});
},
@computed()
showUserDirectoryLink() {
if (!this.siteSettings.enable_user_directory) return false;
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false;
return true;
},
actions: {
keyboardShortcuts() {
this.sendAction('showKeyboardAction');

View File

@ -4,9 +4,9 @@ export default Ember.Component.extend({
tagName: 'li',
classNameBindings: [':header-dropdown-toggle', 'active'],
@computed('showUser')
href(showUser) {
return showUser ? this.currentUser.get('path') : '';
@computed('showUser', 'path')
href(showUser, path) {
return showUser ? this.currentUser.get('path') : Discourse.getURL(path);
},
active: Ember.computed.alias('toggleVisible'),

View File

@ -1,25 +0,0 @@
import { observes, on } from 'ember-addons/ember-computed-decorators';
import loadScript from 'discourse/lib/load-script';
export default Ember.Component.extend({
classNameBindings: [':pagedown-editor'],
@on("didInsertElement")
_initializeWmd() {
loadScript('defer/html-sanitizer-bundle').then(() => {
this.$('.wmd-input').data('init', true);
this._editor = Discourse.Markdown.createEditor({ containerElement: this.element });
this._editor.run();
Ember.run.scheduleOnce('afterRender', this, this._refreshPreview);
});
},
@observes("value")
observeValue() {
Ember.run.scheduleOnce('afterRender', this, this._refreshPreview);
},
_refreshPreview() {
this._editor.refreshPreview();
}
});

View File

@ -9,7 +9,7 @@ export default Ember.Component.extend({
var self = this;
bootbox.dialog(I18n.t("private_message_info.remove_allowed_user", {name: user.get('username')}), [
{label: I18n.t("no_value"),
'class': 'btn-danger rightg'},
'class': 'btn-danger right'},
{label: I18n.t("yes_value"),
'class': 'btn-primary',
callback: function() {

View File

@ -18,36 +18,6 @@ export default Ember.Component.extend({
}
},
signupMethodsTranslated: function() {
const methods = Ember.get('Discourse.LoginMethod.all');
const loginWithEmail = this.siteSettings.enable_local_logins;
if (this.siteSettings.enable_sso) {
return I18n.t('signup_cta.methods.sso');
} else if (methods.length === 0) {
if (loginWithEmail) {
return I18n.t('signup_cta.methods.only_email');
} else {
return I18n.t('signup_cta.methods.unknown');
}
} else if (methods.length === 1) {
let providerName = methods[0].name.capitalize();
if (providerName === "Google_oauth2") {
providerName = "Google";
}
if (loginWithEmail) {
return I18n.t('signup_cta.methods.one_and_email', {provider: providerName});
} else {
return I18n.t('signup_cta.methods.only_other', {provider: providerName});
}
} else {
if (loginWithEmail) {
return I18n.t('signup_cta.methods.multiple', {count: methods.length});
} else {
return I18n.t('signup_cta.methods.multiple_no_email', {count: methods.length});
}
}
}.property(),
_turnOffIfHidden: function() {
if (this.session.get('hideSignupCta')) {
this.session.set('showSignupCta', false);

View File

@ -21,8 +21,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
@computed()
allowImageUpload() {
return Discourse.Utilities.allowsImages();
allowAvatarUpload() {
return this.siteSettings.allow_uploaded_avatars && Discourse.Utilities.allowsImages();
},
actions: {

View File

@ -1,6 +1,7 @@
import debounce from 'discourse/lib/debounce';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { setting } from 'discourse/lib/computed';
import { on } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend(ModalFunctionality, {
needs: ['login'],
@ -78,10 +79,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
// Validate the name.
nameValidation: function() {
if (this.get('accountPasswordConfirm') === 0) {
this.fetchConfirmationValue();
}
if (Discourse.SiteSettings.full_name_required && Ember.isEmpty(this.get('accountName'))) {
return Discourse.InputValidation.create({ failed: true });
}
@ -335,11 +332,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail'),
@on('init')
fetchConfirmationValue() {
const createAccountController = this;
return Discourse.ajax('/users/hp.json').then(function (json) {
createAccountController.set('accountPasswordConfirm', json.value);
createAccountController.set('accountChallenge', json.challenge.split("").reverse().join(""));
return Discourse.ajax('/users/hp.json').then(json => {
this.set('accountPasswordConfirm', json.value);
this.set('accountChallenge', json.challenge.split("").reverse().join(""));
});
},

View File

@ -12,7 +12,7 @@ export var queryParams = {
// Basic controller options
var controllerOpts = {
needs: ['discovery/topics'],
queryParams: Ember.keys(queryParams)
queryParams: Ember.keys(queryParams),
};
// Aliases for the values

View File

@ -2,6 +2,7 @@ import DiscoveryController from 'discourse/controllers/discovery';
import { queryParams } from 'discourse/controllers/discovery-sortable';
import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection';
import { endWith } from 'discourse/lib/computed';
import showModal from 'discourse/lib/show-modal';
const controllerOpts = {
needs: ['discovery'],
@ -66,10 +67,13 @@ const controllerOpts = {
});
},
resetNew() {
this.topicTrackingState.resetNew();
Discourse.Topic.resetNew().then(() => this.send('refresh'));
},
dismissReadPosts() {
showModal('dismiss-read', { title: 'topics.bulk.dismiss_read' });
}
},

View File

@ -11,7 +11,7 @@ export default Ember.Controller.extend({
this.set("loading", true);
Discourse.Group.loadMembers(this.get("name"), this.get("model.members.length"), this.get("limit")).then(result => {
Discourse.Group.loadMembers(this.get("model.name"), this.get("model.members.length"), this.get("limit")).then(result => {
this.get("model.members").addObjects(result.members.map(member => Discourse.User.create(member)));
this.setProperties({
loading: false,

View File

@ -68,12 +68,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
// Show Groups? (add invited user to private group)
showGroups: function() {
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && !this.get('isMessage');
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
// Instructional text for the modal.
inviteInstructions: function() {
if (Discourse.SiteSettings.enable_sso) {
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
// inviting existing user when SSO enabled
return I18n.t('topic.invite_reply.sso_enabled');
} else if (this.get('isMessage')) {
@ -128,7 +128,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
}.property('isMessage'),
placeholderKey: function() {
return Discourse.SiteSettings.enable_sso ?
return (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) ?
'topic.invite_reply.username_placeholder' :
'topic.invite_private.email_or_username_placeholder';
}.property(),

View File

@ -78,9 +78,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
const $hidden_login_form = $('#hidden-login-form');
const destinationUrl = $.cookie('destination_url');
const shouldRedirectToUrl = self.session.get("shouldRedirectToUrl");
const ssoDestinationUrl = $.cookie('sso_destination_url');
$hidden_login_form.find('input[name=username]').val(self.get('loginName'));
$hidden_login_form.find('input[name=password]').val(self.get('loginPassword'));
if (destinationUrl) {
if (ssoDestinationUrl) {
$.cookie('sso_destination_url', null);
window.location.assign(ssoDestinationUrl);
return;
} else if (destinationUrl) {
// redirect client to the original URL
$.cookie('destination_url', null);
$hidden_login_form.find('input[name=redirect]').val(destinationUrl);

View File

@ -5,12 +5,6 @@ import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(CanCheckEmails, {
allowAvatarUpload: setting('allow_uploaded_avatars'),
allowUserLocale: setting('allow_user_locale'),
ssoOverridesAvatar: setting('sso_overrides_avatar'),
allowBackgrounds: setting('allow_profile_backgrounds'),
editHistoryVisible: setting('edit_history_visible_to_public'),
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
selectedCategories(watched, tracked, muted) {
return [].concat(watched, tracked, muted);
@ -45,7 +39,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
@computed()
nameInstructions() {
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
},
@computed("model.has_title_badges")

View File

@ -101,8 +101,10 @@ export default Ember.Controller.extend({
// defer load if needed, if in an expanded replies section
if (!post) {
const postStream = this.get('controllers.topic.model.postStream');
postStream.loadPost(postId).then(() => this.quoteText());
return;
return postStream.loadPost(postId).then(p => {
this.set('post', p);
return this.quoteText();
});
}
// If we can't create a post, delegate to reply as new topic

View File

@ -13,12 +13,13 @@ addBulkButton('closeTopics', 'close_topics');
addBulkButton('archiveTopics', 'archive_topics');
addBulkButton('showNotificationLevel', 'notification_level');
addBulkButton('resetRead', 'reset_read');
addBulkButton('unlistTopics', 'unlist_topics');
// Modal for performing bulk actions on topics
export default Ember.ArrayController.extend(ModalFunctionality, {
buttonRows: null,
onShow: function() {
onShow() {
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal small');
const buttonRows = [];
@ -36,87 +37,80 @@ export default Ember.ArrayController.extend(ModalFunctionality, {
this.send('changeBulkTemplate', 'modal/bulk_actions_buttons');
},
perform: function(operation) {
perform(operation) {
this.set('loading', true);
var self = this,
topics = this.get('model');
return Discourse.Topic.bulkOperation(this.get('model'), operation).then(function(result) {
self.set('loading', false);
const topics = this.get('model');
return Discourse.Topic.bulkOperation(this.get('model'), operation).then(result => {
this.set('loading', false);
if (result && result.topic_ids) {
return result.topic_ids.map(function (t) {
return topics.findBy('id', t);
});
return result.topic_ids.map(t => topics.findBy('id', t));
}
return result;
}).catch(function() {
}).catch(() => {
bootbox.alert(I18n.t('generic_error'));
self.set('loading', false);
this.set('loading', false);
});
},
forEachPerformed: function(operation, cb) {
var self = this;
this.perform(operation).then(function (topics) {
forEachPerformed(operation, cb) {
this.perform(operation).then(topics => {
if (topics) {
topics.forEach(cb);
const refreshTarget = self.get('refreshTarget');
const refreshTarget = this.get('refreshTarget');
if (refreshTarget) { refreshTarget.send('refresh'); }
self.send('closeModal');
this.send('closeModal');
}
});
},
performAndRefresh: function(operation) {
const self = this;
return this.perform(operation).then(function() {
const refreshTarget = self.get('refreshTarget');
performAndRefresh(operation) {
return this.perform(operation).then(() => {
const refreshTarget = this.get('refreshTarget');
if (refreshTarget) { refreshTarget.send('refresh'); }
self.send('closeModal');
this.send('closeModal');
});
},
actions: {
showChangeCategory: function() {
showChangeCategory() {
this.send('changeBulkTemplate', 'modal/bulk_change_category');
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal full');
},
showNotificationLevel: function() {
showNotificationLevel() {
this.send('changeBulkTemplate', 'modal/bulk_notification_level');
},
deleteTopics: function() {
deleteTopics() {
this.performAndRefresh({type: 'delete'});
},
closeTopics: function() {
this.forEachPerformed({type: 'close'}, function(t) {
t.set('closed', true);
});
closeTopics() {
this.forEachPerformed({type: 'close'}, t => t.set('closed', true));
},
archiveTopics: function() {
this.forEachPerformed({type: 'archive'}, function(t) {
t.set('archived', true);
});
archiveTopics() {
this.forEachPerformed({type: 'archive'}, t => t.set('archived', true));
},
changeCategory: function() {
var categoryId = parseInt(this.get('newCategoryId'), 10) || 0,
category = Discourse.Category.findById(categoryId),
self = this;
this.perform({type: 'change_category', category_id: categoryId}).then(function(topics) {
topics.forEach(function(t) {
t.set('category', category);
});
const refreshTarget = self.get('refreshTarget');
unlistTopics() {
this.forEachPerformed({type: 'unlist'}, t => t.set('visible', false));
},
changeCategory() {
const categoryId = parseInt(this.get('newCategoryId'), 10) || 0;
const category = Discourse.Category.findById(categoryId);
this.perform({type: 'change_category', category_id: categoryId}).then(topics => {
topics.forEach(t => t.set('category', category));
const refreshTarget = this.get('refreshTarget');
if (refreshTarget) { refreshTarget.send('refresh'); }
self.send('closeModal');
this.send('closeModal');
});
},
resetRead: function() {
resetRead() {
this.performAndRefresh({ type: 'reset_read' });
}
}

View File

@ -153,7 +153,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
if (user.get('staff') && replyCount > 0) {
bootbox.dialog(I18n.t("post.controls.delete_replies.confirm", {count: replyCount}), [
{label: I18n.t("cancel"),
'class': 'btn-danger rightg'},
'class': 'btn-danger right'},
{label: I18n.t("post.controls.delete_replies.no_value"),
callback() {
post.destroy(user);
@ -374,10 +374,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
togglePinnedForUser() {
if (this.get('model.pinned_at')) {
if (this.get('pinned')) {
this.get('content').clearPin();
const topic = this.get('content');
if (topic.get('pinned')) {
topic.clearPin();
} else {
this.get('content').rePin();
topic.rePin();
}
}
},

View File

@ -39,6 +39,11 @@ export default Ember.Controller.extend({
// XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
// No user card for anon
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
return;
}
// Don't show on mobile
if (Discourse.Mobile.mobileView) {
const url = "/users/" + username;

View File

@ -9,7 +9,15 @@ export default {
window.PagedownCustom.appendButtons.push({
id: 'wmd-emoji-button',
description: I18n.t("composer.emoji"),
execute: showSelector
execute() {
showSelector({
container,
onSelect(title) {
const composerController = container.lookup('controller:composer');
composerController.appendTextAtCursor(`:${title}:`, {space: true});
},
});
}
});
}
}

View File

@ -1,11 +1,12 @@
export function loadAllHelpers() {
Ember.keys(requirejs.entries).forEach(entry => {
if ((/\/helpers\//).test(entry)) {
require(entry, null, null, true);
}
});
}
export default {
name: 'load-all-helpers',
initialize: function() {
Ember.keys(requirejs.entries).forEach(function(entry) {
if ((/\/helpers\//).test(entry)) {
require(entry, null, null, true);
}
});
}
initialize: loadAllHelpers
};

View File

@ -0,0 +1,57 @@
// note that these categories are copied from Slack
// be careful, there are ~20 differences in synonyms, e.g. :boom: vs. :collision:
// a few Emoji are actually missing from the Slack categories as well (?), and were added
const groups = [
{
name: "people",
fullname: "People",
tabicon: "grinning",
icons: ["grinning", "grin", "joy", "smiley", "smile", "sweat_smile", "laughing", "innocent", "smiling_imp", "imp", "wink", "blush", "relaxed", "yum", "relieved", "heart_eyes", "sunglasses", "smirk", "neutral_face", "expressionless", "unamused", "sweat", "pensive", "confused", "confounded", "kissing", "kissing_heart", "kissing_smiling_eyes", "kissing_closed_eyes", "stuck_out_tongue", "stuck_out_tongue_winking_eye", "stuck_out_tongue_closed_eyes", "disappointed", "worried", "angry", "rage", "cry", "persevere", "triumph", "disappointed_relieved", "frowning", "anguished", "fearful", "weary", "sleepy", "tired_face", "grimacing", "sob", "open_mouth", "hushed", "cold_sweat", "scream", "astonished", "flushed", "sleeping", "dizzy_face", "no_mouth", "mask", "smile_cat", "joy_cat", "smiley_cat", "heart_eyes_cat", "smirk_cat", "kissing_cat", "pouting_cat", "crying_cat_face", "scream_cat", "footprints", "bust_in_silhouette", "busts_in_silhouette", "baby", "boy", "girl", "man", "woman", "family", "couple", "two_men_holding_hands", "two_women_holding_hands", "dancers", "bride_with_veil", "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban", "older_man", "older_woman", "cop", "construction_worker", "princess", "guardsman", "angel", "santa", "ghost", "japanese_ogre", "japanese_goblin", "hankey", "skull", "alien", "space_invader", "bow", "information_desk_person", "no_good", "ok_woman", "raising_hand", "person_with_pouting_face", "person_frowning", "massage", "haircut", "couple_with_heart", "couplekiss", "raised_hands", "clap", "hand", "ear", "eyes", "nose", "lips", "kiss", "tongue", "nail_care", "wave", "+1", "-1", "point_up", "point_up_2", "point_down", "point_left", "point_right", "ok_hand", "v", "facepunch", "fist", "raised_hand", "muscle", "open_hands", "pray"]
},
{
name: "nature",
fullname: "Nature",
tabicon: "evergreen_tree",
icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"]
},
{
name: "food",
fullname: "Food & Drink",
tabicon: "hamburger",
icons: ["tomato", "eggplant", "corn", "sweet_potato", "grapes", "melon", "watermelon", "tangerine", "lemon", "banana", "pineapple", "apple", "green_apple", "pear", "peach", "cherries", "strawberry", "hamburger", "pizza", "meat_on_bone", "poultry_leg", "rice_cracker", "rice_ball", "rice", "curry", "ramen", "spaghetti", "bread", "fries", "dango", "oden", "sushi", "fried_shrimp", "fish_cake", "icecream", "shaved_ice", "ice_cream", "doughnut", "cookie", "chocolate_bar", "candy", "lollipop", "custard", "honey_pot", "cake", "bento", "stew", "egg", "fork_and_knife", "tea", "coffee", "sake", "wine_glass", "cocktail", "tropical_drink", "beer", "beers", "baby_bottle"]
},
{
name: "celebration",
fullname: "Celebration",
tabicon: "gift",
icons: ["ribbon", "gift", "birthday", "jack_o_lantern", "christmas_tree", "tanabata_tree", "bamboo", "rice_scene", "fireworks", "sparkler", "tada", "confetti_ball", "balloon", "dizzy", "sparkles", "boom", "mortar_board", "crown", "dolls", "flags", "wind_chime", "crossed_flags", "izakaya_lantern", "ring", "heart", "broken_heart", "love_letter", "two_hearts", "revolving_hearts", "heartbeat", "heartpulse", "sparkling_heart", "cupid", "gift_heart", "heart_decoration", "purple_heart", "yellow_heart", "green_heart", "blue_heart"]
},
{
name: "activity",
fullname: "Activities",
tabicon: "soccer",
icons: ["runner", "walking", "dancer", "rowboat", "swimmer", "surfer", "bath", "snowboarder", "ski", "snowman", "bicyclist", "mountain_bicyclist", "horse_racing", "tent", "fishing_pole_and_fish", "soccer", "basketball", "football", "baseball", "tennis", "rugby_football", "golf", "trophy", "running_shirt_with_sash", "checkered_flag", "musical_keyboard", "guitar", "violin", "saxophone", "trumpet", "musical_note", "notes", "musical_score", "headphones", "microphone", "performing_arts", "ticket", "tophat", "circus_tent", "clapper", "art", "dart", "8ball", "bowling", "slot_machine", "game_die", "video_game", "flower_playing_cards", "black_joker", "mahjong", "carousel_horse", "ferris_wheel", "roller_coaster"]
},
{
name: "travel",
fullname: "Travel & Places",
tabicon: "airplane",
icons: ["train", "mountain_railway", "railway_car", "steam_locomotive", "monorail", "bullettrain_side", "bullettrain_front", "train2", "metro", "light_rail", "station", "tram", "bus", "oncoming_bus", "trolleybus", "minibus", "ambulance", "fire_engine", "police_car", "oncoming_police_car", "rotating_light", "taxi", "oncoming_taxi", "car", "oncoming_automobile", "blue_car", "truck", "articulated_lorry", "tractor", "bike", "busstop", "fuelpump", "construction", "vertical_traffic_light", "traffic_light", "rocket", "helicopter", "airplane", "seat", "anchor", "ship", "speedboat", "boat", "aerial_tramway", "mountain_cableway", "suspension_railway", "passport_control", "customs", "baggage_claim", "left_luggage", "yen", "euro", "pound", "dollar", "statue_of_liberty", "moyai", "foggy", "tokyo_tower", "fountain", "european_castle", "japanese_castle", "city_sunrise", "city_sunset", "night_with_stars", "bridge_at_night", "house", "house_with_garden", "office", "department_store", "factory", "post_office", "european_post_office", "hospital", "bank", "hotel", "love_hotel", "wedding", "church", "convenience_store", "school", "cn", "de", "es", "fr", "gb", "it", "jp", "kr", "ru", "us"]
},
{
name: "objects",
fullname: "Objects & Symbols",
tabicon: "eyeglasses",
icons: ["watch", "iphone", "calling", "computer", "alarm_clock", "hourglass_flowing_sand", "hourglass", "camera", "video_camera", "movie_camera", "tv", "radio", "pager", "telephone_receiver", "phone", "fax", "minidisc", "floppy_disk", "cd", "dvd", "vhs", "battery", "electric_plug", "bulb", "flashlight", "satellite", "credit_card", "money_with_wings", "moneybag", "gem", "closed_umbrella", "pouch", "purse", "handbag", "briefcase", "school_satchel", "lipstick", "eyeglasses", "womans_hat", "sandal", "high_heel", "boot", "mans_shoe", "athletic_shoe", "bikini", "dress", "kimono", "womans_clothes", "shirt", "necktie", "jeans", "door", "shower", "bathtub", "toilet", "barber", "syringe", "pill", "microscope", "telescope", "crystal_ball", "wrench", "hocho", "nut_and_bolt", "hammer", "bomb", "smoking", "gun", "bookmark", "newspaper", "key", "email", "envelope_with_arrow", "incoming_envelope", "e-mail", "inbox_tray", "outbox_tray", "package", "postal_horn", "postbox", "mailbox_closed", "mailbox", "mailbox_with_mail", "mailbox_with_no_mail", "page_facing_up", "page_with_curl", "bookmark_tabs", "chart_with_upwards_trend", "chart_with_downwards_trend", "bar_chart", "date", "calendar", "low_brightness", "high_brightness", "scroll", "clipboard", "book", "notebook", "notebook_with_decorative_cover", "ledger", "closed_book", "green_book", "blue_book", "orange_book", "books", "card_index", "link", "paperclip", "pushpin", "scissors", "triangular_ruler", "round_pushpin", "straight_ruler", "triangular_flag_on_post", "file_folder", "open_file_folder", "black_nib", "pencil2", "memo", "lock_with_ink_pen", "closed_lock_with_key", "lock", "unlock", "mega", "loudspeaker", "sound", "loud_sound", "speaker", "mute", "zzz", "bell", "no_bell", "thought_balloon", "speech_balloon", "children_crossing", "mag", "mag_right", "no_entry_sign", "no_entry", "name_badge", "no_pedestrians", "do_not_litter", "no_bicycles", "non-potable_water", "no_mobile_phones", "underage", "accept", "ideograph_advantage", "white_flower", "secret", "congratulations", "u5408", "u6e80", "u7981", "u6709", "u7121", "u7533", "u55b6", "u6708", "u5272", "u7a7a", "sa", "koko", "u6307", "chart", "sparkle", "eight_spoked_asterisk", "negative_squared_cross_mark", "white_check_mark", "eight_pointed_black_star", "vibration_mode", "mobile_phone_off", "vs", "a", "b", "ab", "cl", "o2", "sos", "id", "parking", "wc", "cool", "free", "new", "ng", "ok", "up", "atm", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "restroom", "mens", "womens", "baby_symbol", "wheelchair", "potable_water", "no_smoking", "put_litter_in_its_place", "arrow_forward", "arrow_backward", "arrow_up_small", "arrow_down_small", "fast_forward", "rewind", "arrow_double_up", "arrow_double_down", "arrow_right", "arrow_left", "arrow_up", "arrow_down", "arrow_upper_right", "arrow_lower_right", "arrow_lower_left", "arrow_upper_left", "arrow_up_down", "left_right_arrow", "arrows_counterclockwise", "arrow_right_hook", "leftwards_arrow_with_hook", "arrow_heading_up", "arrow_heading_down", "twisted_rightwards_arrows", "repeat", "repeat_one", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "1234", "hash", "abc", "abcd", "capital_abcd", "information_source", "signal_strength", "cinema", "symbols", "heavy_plus_sign", "heavy_minus_sign", "wavy_dash", "heavy_division_sign", "heavy_multiplication_x", "heavy_check_mark", "arrows_clockwise", "tm", "copyright", "registered", "currency_exchange", "heavy_dollar_sign", "curly_loop", "loop", "part_alternation_mark", "exclamation", "bangbang", "question", "grey_exclamation", "grey_question", "interrobang", "x", "o", "100", "end", "back", "on", "top", "soon", "cyclone", "m", "ophiuchus", "six_pointed_star", "beginner", "trident", "warning", "hotsprings", "recycle", "anger", "diamond_shape_with_a_dot_inside", "spades", "clubs", "hearts", "diamonds", "ballot_box_with_check", "white_circle", "black_circle", "radio_button", "red_circle", "large_blue_circle", "small_red_triangle", "small_red_triangle_down", "small_orange_diamond", "small_blue_diamond", "large_orange_diamond", "large_blue_diamond", "black_small_square", "white_small_square", "black_large_square", "white_large_square", "black_medium_square", "white_medium_square", "black_medium_small_square", "white_medium_small_square", "black_square_button", "white_square_button", "clock1", "clock2", "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9", "clock10", "clock11", "clock12", "clock130", "clock230", "clock330", "clock430", "clock530", "clock630", "clock730", "clock830", "clock930", "clock1030", "clock1130", "clock1230"]
}
];
// scrub groups
groups.forEach(group => {
group.icons = group.icons.reject(obj => !Discourse.Emoji.exists(obj));
});
// export so others can modify
Discourse.Emoji.groups = groups;
export default groups;

View File

@ -1,101 +1,45 @@
import groups from 'discourse/lib/emoji/emoji-groups';
import KeyValueStore from "discourse/lib/key-value-store";
const keyValueStore = new KeyValueStore("discourse_emojis_");
const EMOJI_USAGE = "emojiUsage";
// note that these categories are copied from Slack
// be careful, there are ~20 differences in synonyms, e.g. :boom: vs. :collision:
// a few Emoji are actually missing from the Slack categories as well (?), and were added
var groups = [
{
name: "people",
fullname: "People",
tabicon: "grinning",
icons: ["grinning", "grin", "joy", "smiley", "smile", "sweat_smile", "laughing", "innocent", "smiling_imp", "imp", "wink", "blush", "relaxed", "yum", "relieved", "heart_eyes", "sunglasses", "smirk", "neutral_face", "expressionless", "unamused", "sweat", "pensive", "confused", "confounded", "kissing", "kissing_heart", "kissing_smiling_eyes", "kissing_closed_eyes", "stuck_out_tongue", "stuck_out_tongue_winking_eye", "stuck_out_tongue_closed_eyes", "disappointed", "worried", "angry", "rage", "cry", "persevere", "triumph", "disappointed_relieved", "frowning", "anguished", "fearful", "weary", "sleepy", "tired_face", "grimacing", "sob", "open_mouth", "hushed", "cold_sweat", "scream", "astonished", "flushed", "sleeping", "dizzy_face", "no_mouth", "mask", "smile_cat", "joy_cat", "smiley_cat", "heart_eyes_cat", "smirk_cat", "kissing_cat", "pouting_cat", "crying_cat_face", "scream_cat", "footprints", "bust_in_silhouette", "busts_in_silhouette", "baby", "boy", "girl", "man", "woman", "family", "couple", "two_men_holding_hands", "two_women_holding_hands", "dancers", "bride_with_veil", "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban", "older_man", "older_woman", "cop", "construction_worker", "princess", "guardsman", "angel", "santa", "ghost", "japanese_ogre", "japanese_goblin", "hankey", "skull", "alien", "space_invader", "bow", "information_desk_person", "no_good", "ok_woman", "raising_hand", "person_with_pouting_face", "person_frowning", "massage", "haircut", "couple_with_heart", "couplekiss", "raised_hands", "clap", "hand", "ear", "eyes", "nose", "lips", "kiss", "tongue", "nail_care", "wave", "+1", "-1", "point_up", "point_up_2", "point_down", "point_left", "point_right", "ok_hand", "v", "facepunch", "fist", "raised_hand", "muscle", "open_hands", "pray"]
},
{
name: "nature",
fullname: "Nature",
tabicon: "evergreen_tree",
icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"]
},
{
name: "food",
fullname: "Food & Drink",
tabicon: "hamburger",
icons: ["tomato", "eggplant", "corn", "sweet_potato", "grapes", "melon", "watermelon", "tangerine", "lemon", "banana", "pineapple", "apple", "green_apple", "pear", "peach", "cherries", "strawberry", "hamburger", "pizza", "meat_on_bone", "poultry_leg", "rice_cracker", "rice_ball", "rice", "curry", "ramen", "spaghetti", "bread", "fries", "dango", "oden", "sushi", "fried_shrimp", "fish_cake", "icecream", "shaved_ice", "ice_cream", "doughnut", "cookie", "chocolate_bar", "candy", "lollipop", "custard", "honey_pot", "cake", "bento", "stew", "egg", "fork_and_knife", "tea", "coffee", "sake", "wine_glass", "cocktail", "tropical_drink", "beer", "beers", "baby_bottle"]
},
{
name: "celebration",
fullname: "Celebration",
tabicon: "gift",
icons: ["ribbon", "gift", "birthday", "jack_o_lantern", "christmas_tree", "tanabata_tree", "bamboo", "rice_scene", "fireworks", "sparkler", "tada", "confetti_ball", "balloon", "dizzy", "sparkles", "boom", "mortar_board", "crown", "dolls", "flags", "wind_chime", "crossed_flags", "izakaya_lantern", "ring", "heart", "broken_heart", "love_letter", "two_hearts", "revolving_hearts", "heartbeat", "heartpulse", "sparkling_heart", "cupid", "gift_heart", "heart_decoration", "purple_heart", "yellow_heart", "green_heart", "blue_heart"]
},
{
name: "activity",
fullname: "Activities",
tabicon: "soccer",
icons: ["runner", "walking", "dancer", "rowboat", "swimmer", "surfer", "bath", "snowboarder", "ski", "snowman", "bicyclist", "mountain_bicyclist", "horse_racing", "tent", "fishing_pole_and_fish", "soccer", "basketball", "football", "baseball", "tennis", "rugby_football", "golf", "trophy", "running_shirt_with_sash", "checkered_flag", "musical_keyboard", "guitar", "violin", "saxophone", "trumpet", "musical_note", "notes", "musical_score", "headphones", "microphone", "performing_arts", "ticket", "tophat", "circus_tent", "clapper", "art", "dart", "8ball", "bowling", "slot_machine", "game_die", "video_game", "flower_playing_cards", "black_joker", "mahjong", "carousel_horse", "ferris_wheel", "roller_coaster"]
},
{
name: "travel",
fullname: "Travel & Places",
tabicon: "airplane",
icons: ["train", "mountain_railway", "railway_car", "steam_locomotive", "monorail", "bullettrain_side", "bullettrain_front", "train2", "metro", "light_rail", "station", "tram", "bus", "oncoming_bus", "trolleybus", "minibus", "ambulance", "fire_engine", "police_car", "oncoming_police_car", "rotating_light", "taxi", "oncoming_taxi", "car", "oncoming_automobile", "blue_car", "truck", "articulated_lorry", "tractor", "bike", "busstop", "fuelpump", "construction", "vertical_traffic_light", "traffic_light", "rocket", "helicopter", "airplane", "seat", "anchor", "ship", "speedboat", "boat", "aerial_tramway", "mountain_cableway", "suspension_railway", "passport_control", "customs", "baggage_claim", "left_luggage", "yen", "euro", "pound", "dollar", "statue_of_liberty", "moyai", "foggy", "tokyo_tower", "fountain", "european_castle", "japanese_castle", "city_sunrise", "city_sunset", "night_with_stars", "bridge_at_night", "house", "house_with_garden", "office", "department_store", "factory", "post_office", "european_post_office", "hospital", "bank", "hotel", "love_hotel", "wedding", "church", "convenience_store", "school", "cn", "de", "es", "fr", "gb", "it", "jp", "kr", "ru", "us"]
},
{
name: "objects",
fullname: "Objects & Symbols",
tabicon: "eyeglasses",
icons: ["watch", "iphone", "calling", "computer", "alarm_clock", "hourglass_flowing_sand", "hourglass", "camera", "video_camera", "movie_camera", "tv", "radio", "pager", "telephone_receiver", "phone", "fax", "minidisc", "floppy_disk", "cd", "dvd", "vhs", "battery", "electric_plug", "bulb", "flashlight", "satellite", "credit_card", "money_with_wings", "moneybag", "gem", "closed_umbrella", "pouch", "purse", "handbag", "briefcase", "school_satchel", "lipstick", "eyeglasses", "womans_hat", "sandal", "high_heel", "boot", "mans_shoe", "athletic_shoe", "bikini", "dress", "kimono", "womans_clothes", "shirt", "necktie", "jeans", "door", "shower", "bathtub", "toilet", "barber", "syringe", "pill", "microscope", "telescope", "crystal_ball", "wrench", "hocho", "nut_and_bolt", "hammer", "bomb", "smoking", "gun", "bookmark", "newspaper", "key", "email", "envelope_with_arrow", "incoming_envelope", "e-mail", "inbox_tray", "outbox_tray", "package", "postal_horn", "postbox", "mailbox_closed", "mailbox", "mailbox_with_mail", "mailbox_with_no_mail", "page_facing_up", "page_with_curl", "bookmark_tabs", "chart_with_upwards_trend", "chart_with_downwards_trend", "bar_chart", "date", "calendar", "low_brightness", "high_brightness", "scroll", "clipboard", "book", "notebook", "notebook_with_decorative_cover", "ledger", "closed_book", "green_book", "blue_book", "orange_book", "books", "card_index", "link", "paperclip", "pushpin", "scissors", "triangular_ruler", "round_pushpin", "straight_ruler", "triangular_flag_on_post", "file_folder", "open_file_folder", "black_nib", "pencil2", "memo", "lock_with_ink_pen", "closed_lock_with_key", "lock", "unlock", "mega", "loudspeaker", "sound", "loud_sound", "speaker", "mute", "zzz", "bell", "no_bell", "thought_balloon", "speech_balloon", "children_crossing", "mag", "mag_right", "no_entry_sign", "no_entry", "name_badge", "no_pedestrians", "do_not_litter", "no_bicycles", "non-potable_water", "no_mobile_phones", "underage", "accept", "ideograph_advantage", "white_flower", "secret", "congratulations", "u5408", "u6e80", "u7981", "u6709", "u7121", "u7533", "u55b6", "u6708", "u5272", "u7a7a", "sa", "koko", "u6307", "chart", "sparkle", "eight_spoked_asterisk", "negative_squared_cross_mark", "white_check_mark", "eight_pointed_black_star", "vibration_mode", "mobile_phone_off", "vs", "a", "b", "ab", "cl", "o2", "sos", "id", "parking", "wc", "cool", "free", "new", "ng", "ok", "up", "atm", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "restroom", "mens", "womens", "baby_symbol", "wheelchair", "potable_water", "no_smoking", "put_litter_in_its_place", "arrow_forward", "arrow_backward", "arrow_up_small", "arrow_down_small", "fast_forward", "rewind", "arrow_double_up", "arrow_double_down", "arrow_right", "arrow_left", "arrow_up", "arrow_down", "arrow_upper_right", "arrow_lower_right", "arrow_lower_left", "arrow_upper_left", "arrow_up_down", "left_right_arrow", "arrows_counterclockwise", "arrow_right_hook", "leftwards_arrow_with_hook", "arrow_heading_up", "arrow_heading_down", "twisted_rightwards_arrows", "repeat", "repeat_one", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "1234", "hash", "abc", "abcd", "capital_abcd", "information_source", "signal_strength", "cinema", "symbols", "heavy_plus_sign", "heavy_minus_sign", "wavy_dash", "heavy_division_sign", "heavy_multiplication_x", "heavy_check_mark", "arrows_clockwise", "tm", "copyright", "registered", "currency_exchange", "heavy_dollar_sign", "curly_loop", "loop", "part_alternation_mark", "exclamation", "bangbang", "question", "grey_exclamation", "grey_question", "interrobang", "x", "o", "100", "end", "back", "on", "top", "soon", "cyclone", "m", "ophiuchus", "six_pointed_star", "beginner", "trident", "warning", "hotsprings", "recycle", "anger", "diamond_shape_with_a_dot_inside", "spades", "clubs", "hearts", "diamonds", "ballot_box_with_check", "white_circle", "black_circle", "radio_button", "red_circle", "large_blue_circle", "small_red_triangle", "small_red_triangle_down", "small_orange_diamond", "small_blue_diamond", "large_orange_diamond", "large_blue_diamond", "black_small_square", "white_small_square", "black_large_square", "white_large_square", "black_medium_square", "white_medium_square", "black_medium_small_square", "white_medium_small_square", "black_square_button", "white_square_button", "clock1", "clock2", "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9", "clock10", "clock11", "clock12", "clock130", "clock230", "clock330", "clock430", "clock530", "clock630", "clock730", "clock830", "clock930", "clock1030", "clock1130", "clock1230"]
}
];
let PER_ROW = 12;
const PER_PAGE = 60;
// scrub groups
groups.forEach(function(group){
group.icons = _.reject(group.icons, function(obj){
return !Discourse.Emoji.exists(obj);
});
});
// export so others can modify
Discourse.Emoji.groups = groups;
var closeSelector = function(){
$('.emoji-modal, .emoji-modal-wrapper').remove();
$('body, textarea').off('keydown.emoji');
};
var ungroupedIcons, recentlyUsedIcons;
var initializeUngroupedIcons = function(){
ungroupedIcons = [];
var groupedIcons = {};
_.each(groups, function(group){
_.each(group.icons, function(icon){
groupedIcons[icon] = true;
});
});
var emojis = Discourse.Emoji.list();
_.each(emojis, function(emoji){
if(groupedIcons[emoji] !== true){
ungroupedIcons.push(emoji);
}
});
if(ungroupedIcons.length > 0){
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
};
let ungroupedIcons, recentlyUsedIcons;
if (!keyValueStore.getObject(EMOJI_USAGE)) {
keyValueStore.setObject({key: EMOJI_USAGE, value: {}});
}
var trackEmojiUsage = function(title) {
var recent = keyValueStore.getObject(EMOJI_USAGE);
function closeSelector() {
$('.emoji-modal, .emoji-modal-wrapper').remove();
$('body, textarea').off('keydown.emoji');
}
function initializeUngroupedIcons() {
const groupedIcons = {};
groups.forEach(group => {
group.icons.forEach(icon => groupedIcons[icon] = true);
});
ungroupedIcons = [];
const emojis = Discourse.Emoji.list();
emojis.forEach(emoji => {
if (groupedIcons[emoji] !== true) {
ungroupedIcons.push(emoji);
}
});
if (ungroupedIcons.length) {
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
}
function trackEmojiUsage(title) {
const recent = keyValueStore.getObject(EMOJI_USAGE);
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
recent[title]["usage"]++;
@ -104,46 +48,40 @@ var trackEmojiUsage = function(title) {
// clear the cache
recentlyUsedIcons = null;
};
}
var initializeRecentlyUsedIcons = function(){
function sortByUsage(a, b) {
if (a.usage > b.usage) { return -1; }
if (b.usage > a.usage) { return 1; }
return a.title.localeCompare(b.title);
}
function initializeRecentlyUsedIcons() {
recentlyUsedIcons = [];
var usage = _.map(keyValueStore.getObject(EMOJI_USAGE));
usage.sort(function(a,b){
if(a.usage > b.usage){
return -1;
const usage = _.map(keyValueStore.getObject(EMOJI_USAGE)).sort(sortByUsage);
const recent = usage.slice(0, PER_ROW);
if (recent.length > 0) {
recent.forEach(emoji => recentlyUsedIcons.push(emoji.title));
const recentGroup = groups.findProperty('name', 'recent');
if (recentGroup) {
recentGroup.icons = recentlyUsedIcons;
} else {
groups.push({ name: 'recent', icons: recentlyUsedIcons });
}
if(b.usage > a.usage){
return 1;
}
return a.title.localeCompare(b.title);
});
var recent = _.take(usage, PER_ROW);
if(recent.length > 0){
_.each(recent, function(emoji){
recentlyUsedIcons.push(emoji.title);
});
var recentGroup = _.find(groups, {name: 'recent'});
if(!recentGroup){
recentGroup = {name: 'recent', icons: []};
groups.push(recentGroup);
}
recentGroup.icons = recentlyUsedIcons;
}
};
}
var toolbar = function(selected){
function toolbar(selected) {
if (!ungroupedIcons) { initializeUngroupedIcons(); }
if (!recentlyUsedIcons) { initializeRecentlyUsedIcons(); }
return _.map(groups, function(g, i){
var icon = g.tabicon;
var title = g.fullname;
return groups.map((g, i) => {
let icon = g.tabicon;
let title = g.fullname;
if (g.name === "recent") {
icon = "star";
title = "Recent";
@ -151,60 +89,48 @@ var toolbar = function(selected){
icon = g.icons[0];
title = "Custom";
}
var row = {src: Discourse.Emoji.urlFor(icon), title: title, groupId: i};
if(i === selected){
row.selected = true;
}
return row;
return { src: Discourse.Emoji.urlFor(icon),
title,
groupId: i,
selected: i === selected };
});
};
}
var PER_ROW = 12, PER_PAGE = 60;
var bindEvents = function(page, offset, options) {
var composerController = Discourse.__container__.lookup('controller:composer');
$('.emoji-page a').click(function(){
var title = $(this).attr('title');
function bindEvents(page, offset, options) {
$('.emoji-page a').click(e => {
const title = $(e.currentTarget).attr('title');
trackEmojiUsage(title);
const prefix = options.skipPrefix ? "" : ":";
composerController.appendTextAtCursor(`${prefix}${title}:`, {space: !options.skipPrefix});
options.onSelect(title);
closeSelector();
return false;
}).hover(function(){
var title = $(this).attr('title');
var html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
}).hover(e => {
const title = $(e.currentTarget).attr('title');
const html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
$('.emoji-modal .info').html(html);
},function(){
$('.emoji-modal .info').html("");
});
}, () => $('.emoji-modal .info').html(""));
$('.emoji-modal .nav .next a').click(function(){
render(page, offset+PER_PAGE, options);
});
$('.emoji-modal .nav .prev a').click(function(){
render(page, offset-PER_PAGE, options);
});
$('.emoji-modal .nav .next a').click(() => render(page, offset+PER_PAGE, options));
$('.emoji-modal .nav .prev a').click(() => render(page, offset-PER_PAGE, options));
$('.emoji-modal .toolbar a').click(function(){
var p = parseInt($(this).data('group-id'));
const p = parseInt($(this).data('group-id'));
render(p, 0, options);
return false;
});
};
}
var render = function(page, offset, options) {
function render(page, offset, options) {
keyValueStore.set({key: "emojiPage", value: page});
keyValueStore.set({key: "emojiOffset", value: offset});
var toolbarItems = toolbar(page);
var rows = [], row = [];
var icons = groups[page].icons;
var max = offset + PER_PAGE;
const toolbarItems = toolbar(page);
const rows = [];
let row = [];
const icons = groups[page].icons;
const max = offset + PER_PAGE;
for(var i=offset; i<max; i++){
for(let i=offset; i<max; i++){
if(!icons[i]){ break; }
if(row.length === PER_ROW){
rows.push(row);
@ -214,41 +140,39 @@ var render = function(page, offset, options) {
}
rows.push(row);
var model = {
const model = {
toolbarItems: toolbarItems,
rows: rows,
prevDisabled: offset === 0,
nextDisabled: (max + 1) > icons.length
};
$('body .emoji-modal').remove();
var rendered = Ember.TEMPLATES["emoji-toolbar.raw"](model);
$('body').append(rendered);
$('.emoji-modal', options.appendTo).remove();
const template = options.container.lookup('template:emoji-toolbar.raw');
options.appendTo.append(template(model));
bindEvents(page, offset, options);
};
}
var showSelector = function(options) {
function showSelector(options) {
options = options || {};
options.appendTo = options.appendTo || $('body');
$('body').append('<div class="emoji-modal-wrapper"></div>');
$('.emoji-modal-wrapper').click(function(){
closeSelector();
});
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
$('.emoji-modal-wrapper').click(() => closeSelector());
if (Discourse.Mobile.mobileView) PER_ROW = 9;
const page = keyValueStore.getInt("emojiPage", 0);
const offset = keyValueStore.getInt("emojiOffset", 0);
var page = keyValueStore.getInt("emojiPage", 0);
var offset = keyValueStore.getInt("emojiOffset", 0);
render(page, offset, options);
$('body, textarea').on('keydown.emoji', function(e){
if(e.which === 27){
$('body, textarea').on('keydown.emoji', e => {
if (e.which === 27) {
closeSelector();
return false;
}
});
};
}
export { showSelector };

View File

@ -12,6 +12,11 @@ Discourse.Dialect.registerEmoji = function(code, url) {
extendedEmoji[code] = url;
};
// This method is used by PrettyText to reset custom emojis in multisites
Discourse.Dialect.resetEmoji = function() {
extendedEmoji = {};
};
Discourse.Emoji.list = function(){
var list = emoji.slice(0);
_.each(extendedEmoji, function(v,k){ list.push(k); });

View File

@ -99,7 +99,7 @@ export default {
$('.topic-post.selected button.create').click();
// lazy but should work for now
setTimeout(function() {
$('#wmd-quote-post').click();
$('.wmd-quote-post').click();
}, 500);
},
@ -358,8 +358,8 @@ export default {
},
_changeSection(direction) {
const $sections = $('#navigation-bar li'),
active = $('#navigation-bar li.active'),
const $sections = $('.nav.nav-pills li'),
active = $('.nav.nav-pills li.active'),
index = $sections.index(active) + direction;
if (index >= 0 && index < $sections.length) {

View File

@ -42,6 +42,7 @@ export default Ember.Mixin.create({
});
tracker.incrementMessageCount();
}
self.send('closeModal');
self.send('refresh');
});
}

View File

@ -201,7 +201,7 @@ const Composer = RestModel.extend({
return this.get('canCategorize') &&
!this.siteSettings.allow_uncategorized_topics &&
!this.get('categoryId') &&
!this.user.get('staff');
!this.user.get('admin');
}
}.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'),

View File

@ -445,8 +445,7 @@ const PostStream = RestModel.extend({
const url = "/posts/" + postId;
const store = this.store;
return Discourse.ajax(url).then((p) =>
this.storePost(store.createRecord('post', p)));
return Discourse.ajax(url).then(p => this.storePost(store.createRecord('post', p)));
},
/**

View File

@ -8,6 +8,22 @@ const Topic = RestModel.extend({
message: null,
errorLoading: false,
@computed('posters.firstObject')
creator(poster){
return poster && poster.user;
},
@computed('posters.lastObject')
lastPoster(poster) {
if (poster){
if (this.last_poster_username === poster.user.username){
return poster.user;
} else {
return this.get('creator');
}
}
},
@computed('fancy_title')
fancyTitle(title) {
title = title || "";

View File

@ -68,6 +68,8 @@ const User = RestModel.extend({
adminPath: url('username_lower', "/admin/users/%@"),
mutedTopicsPath: url('/latest?state=muted'),
@computed("username")
username_lower(username) {
return username.toLowerCase();

View File

@ -7,19 +7,32 @@ export default {
name: 'dynamic-route-builders',
initialize(container, app) {
app.DiscoveryCategoryController = DiscoverySortableController.extend();
app.DiscoveryParentCategoryController = DiscoverySortableController.extend();
app.DiscoveryCategoryNoneController = DiscoverySortableController.extend();
app.DiscoveryCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryParentCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryCategoryNoneRoute = buildCategoryRoute('latest', {no_subcategories: true});
const site = Discourse.Site.current();
site.get('filters').forEach(filter => {
app["Discovery" + filter.capitalize() + "Controller"] = DiscoverySortableController.extend();
app["Discovery" + filter.capitalize() + "Route"] = buildTopicRoute(filter);
app["Discovery" + filter.capitalize() + "CategoryRoute"] = buildCategoryRoute(filter);
app["Discovery" + filter.capitalize() + "CategoryNoneRoute"] = buildCategoryRoute(filter, {no_subcategories: true});
const filterCapitalized = filter.capitalize();
app[`Discovery${filterCapitalized}Controller`] = DiscoverySortableController.extend();
app[`Discovery${filterCapitalized}CategoryController`] = DiscoverySortableController.extend();
app[`Discovery${filterCapitalized}ParentCategoryController`] = DiscoverySortableController.extend();
app[`Discovery${filterCapitalized}CategoryNoneController`] = DiscoverySortableController.extend();
app[`Discovery${filterCapitalized}Route`] = buildTopicRoute(filter);
app[`Discovery${filterCapitalized}CategoryRoute`] = buildCategoryRoute(filter);
app[`Discovery${filterCapitalized}ParentCategoryRoute`] = buildCategoryRoute(filter);
app[`Discovery${filterCapitalized}CategoryNoneRoute`] = buildCategoryRoute(filter, {no_subcategories: true});
});
Discourse.DiscoveryTopController = DiscoverySortableController.extend();
Discourse.DiscoveryTopCategoryController = DiscoverySortableController.extend();
Discourse.DiscoveryTopParentCategoryController = DiscoverySortableController.extend();
Discourse.DiscoveryTopCategoryNoneController = DiscoverySortableController.extend();
Discourse.DiscoveryTopRoute = buildTopicRoute('top', {
actions: {
willTransition() {
@ -30,13 +43,19 @@ export default {
}
});
Discourse.DiscoveryTopCategoryRoute = buildCategoryRoute('top');
Discourse.DiscoveryTopParentCategoryRoute = buildCategoryRoute('top');
Discourse.DiscoveryTopCategoryNoneRoute = buildCategoryRoute('top', {no_subcategories: true});
site.get('periods').forEach(period => {
app["DiscoveryTop" + period.capitalize() + "Controller"] = DiscoverySortableController.extend();
app["DiscoveryTop" + period.capitalize() + "Route"] = buildTopicRoute('top/' + period);
app["DiscoveryTop" + period.capitalize() + "CategoryRoute"] = buildCategoryRoute('top/' + period);
app["DiscoveryTop" + period.capitalize() + "CategoryNoneRoute"] = buildCategoryRoute('top/' + period, {no_subcategories: true});
const periodCapitalized = period.capitalize();
app[`DiscoveryTop${periodCapitalized}Controller`] = DiscoverySortableController.extend();
app[`DiscoveryTop${periodCapitalized}CategoryController`] = DiscoverySortableController.extend();
app[`DiscoveryTop${periodCapitalized}ParentCategoryController`] = DiscoverySortableController.extend();
app[`DiscoveryTop${periodCapitalized}CategoryNoneController`] = DiscoverySortableController.extend();
app[`DiscoveryTop${periodCapitalized}Route`] = buildTopicRoute('top/' + period);
app[`DiscoveryTop${periodCapitalized}CategoryRoute`] = buildCategoryRoute('top/' + period);
app[`DiscoveryTop${periodCapitalized}ParentCategoryRoute`] = buildCategoryRoute('top/' + period);
app[`DiscoveryTop${periodCapitalized}CategoryNoneRoute`] = buildCategoryRoute('top/' + period, {no_subcategories: true});
});
}
};

View File

@ -15,26 +15,25 @@ export default function() {
this.resource('discovery', { path: '/' }, function() {
// top
this.route('top');
this.route('topCategory', { path: '/c/:slug/l/top' });
this.route('topParentCategory', { path: '/c/:slug/l/top' });
this.route('topCategoryNone', { path: '/c/:slug/none/l/top' });
this.route('topCategory', { path: '/c/:parentSlug/:slug/l/top' });
// top by periods
var self = this;
Discourse.Site.currentProp('periods').forEach(function(period) {
var top = 'top' + period.capitalize();
self.route(top, { path: '/top/' + period });
self.route(top + 'Category', { path: '/c/:slug/l/top/' + period });
self.route(top + 'CategoryNone', { path: '/c/:slug/none/l/top/' + period });
self.route(top + 'Category', { path: '/c/:parentSlug/:slug/l/top/' + period });
Discourse.Site.currentProp('periods').forEach(period => {
const top = 'top' + period.capitalize();
this.route(top, { path: '/top/' + period });
this.route(top + 'ParentCategory', { path: '/c/:slug/l/top/' + period });
this.route(top + 'CategoryNone', { path: '/c/:slug/none/l/top/' + period });
this.route(top + 'Category', { path: '/c/:parentSlug/:slug/l/top/' + period });
});
// filters
Discourse.Site.currentProp('filters').forEach(function(filter) {
self.route(filter, { path: '/' + filter });
self.route(filter + 'Category', { path: '/c/:slug/l/' + filter });
self.route(filter + 'CategoryNone', { path: '/c/:slug/none/l/' + filter });
self.route(filter + 'Category', { path: '/c/:parentSlug/:slug/l/' + filter });
Discourse.Site.currentProp('filters').forEach(filter => {
this.route(filter, { path: '/' + filter });
this.route(filter + 'ParentCategory', { path: '/c/:slug/l/' + filter });
this.route(filter + 'CategoryNone', { path: '/c/:slug/none/l/' + filter });
this.route(filter + 'Category', { path: '/c/:parentSlug/:slug/l/' + filter });
});
this.route('categories');
@ -56,9 +55,8 @@ export default function() {
this.resource('users');
this.resource('user', { path: '/users/:username' }, function() {
this.resource('userActivity', { path: '/activity' }, function() {
var self = this;
_.map(Discourse.UserAction.TYPES, function (id, userAction) {
self.route(userAction, { path: userAction.replace('_', '-') });
_.map(Discourse.UserAction.TYPES, (id, userAction) => {
this.route(userAction, { path: userAction.replace('_', '-') });
});
});

View File

@ -1,12 +1,13 @@
import { queryParams, filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route';
import { filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route';
import { queryParams } from 'discourse/controllers/discovery-sortable';
// A helper function to create a category route with parameters
export default (filter, params) => {
return Discourse.Route.extend({
queryParams: queryParams,
queryParams,
model(modelParams) {
return Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug);
return { category: Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug) };
},
afterModel(model, transition) {
@ -15,27 +16,27 @@ export default (filter, params) => {
return;
}
this._setupNavigation(model);
return Em.RSVP.all([this._createSubcategoryList(model),
this._retrieveTopicList(model, transition)]);
this._setupNavigation(model.category);
return Em.RSVP.all([this._createSubcategoryList(model.category),
this._retrieveTopicList(model.category, transition)]);
},
_setupNavigation(model) {
_setupNavigation(category) {
const noSubcategories = params && !!params.no_subcategories,
filterMode = `c/${Discourse.Category.slugFor(model)}${noSubcategories ? "/none" : ""}/l/${filter}`;
filterMode = `c/${Discourse.Category.slugFor(category)}${noSubcategories ? "/none" : ""}/l/${filter}`;
this.controllerFor('navigation/category').setProperties({
category: model,
category,
filterMode: filterMode,
noSubcategories: params && params.no_subcategories,
canEditCategory: model.get('can_edit')
canEditCategory: category.get('can_edit')
});
},
_createSubcategoryList(model) {
_createSubcategoryList(category) {
this._categoryList = null;
if (Em.isNone(model.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
return Discourse.CategoryList.listForParent(this.store, model)
if (Em.isNone(category.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
return Discourse.CategoryList.listForParent(this.store, category)
.then(list => this._categoryList = list);
}
@ -43,49 +44,60 @@ export default (filter, params) => {
return Em.RSVP.resolve();
},
_retrieveTopicList(model, transition) {
const listFilter = `c/${Discourse.Category.slugFor(model)}/l/${filter}`,
_retrieveTopicList(category, transition) {
const listFilter = `c/${Discourse.Category.slugFor(category)}/l/${filter}`,
findOpts = filterQueryParams(transition.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(list => {
Discourse.TopicList.hideUniformCategory(list, model);
Discourse.TopicList.hideUniformCategory(list, category);
this.set('topics', list);
return list;
});
},
titleToken() {
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', { count: 0 }),
model = this.currentModel;
category = this.currentModel.category;
return I18n.t('filters.with_category', { filter: filterText, category: model.get('name') });
return I18n.t('filters.with_category', { filter: filterText, category: category.get('name') });
},
setupController(controller, model) {
const topics = this.get('topics'),
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
category = model.category,
canCreateTopic = topics.get('can_create_topic'),
canCreateTopicOnCategory = model.get('permission') === Discourse.PermissionType.FULL;
canCreateTopicOnCategory = category.get('permission') === Discourse.PermissionType.FULL;
this.controllerFor('navigation/category').setProperties({
canCreateTopicOnCategory: canCreateTopicOnCategory,
cannotCreateTopicOnCategory: !canCreateTopicOnCategory,
canCreateTopic: canCreateTopic
});
this.controllerFor('discovery/topics').setProperties({
var topicOpts = {
model: topics,
category: model,
period: periodId,
category,
period: topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
selected: [],
noSubcategories: params && !!params.no_subcategories,
order: topics.get('params.order'),
ascending: topics.get('params.ascending'),
expandAllPinned: true,
canCreateTopic: canCreateTopic,
canCreateTopicOnCategory: canCreateTopicOnCategory
});
};
this.searchService.set('searchContext', model.get('searchContext'));
const p = category.get('params');
if (p && Object.keys(p).length) {
if (p.order !== undefined) {
topicOpts.order = p.order;
}
if (p.ascending !== undefined) {
topicOpts.ascending = p.ascending;
}
}
this.controllerFor('discovery/topics').setProperties(topicOpts);
this.searchService.set('searchContext', category.get('searchContext'));
this.set('topics', null);
this.openTopicDraft(topics);
@ -100,6 +112,12 @@ export default (filter, params) => {
this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' });
},
resetController(controller, isExiting) {
if (isExiting) {
controller.setProperties({ order: "default", ascending: false });
}
},
deactivate() {
this._super();
this.searchService.set('searchContext', null);

View File

@ -84,18 +84,11 @@ export default function(filter, extras) {
return I18n.t('filters.with_topics', {filter: filterText});
},
setupController(controller, model, trans) {
if (trans) {
controller.setProperties(Em.getProperties(trans, _.keys(queryParams).map(function(v){
return 'queryParams.' + v;
})));
}
const period = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
setupController(controller, model) {
const topicOpts = {
model,
category: null,
period,
period: model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
selected: [],
expandGloballyPinned: true
};
@ -115,6 +108,12 @@ export default function(filter, extras) {
this.controllerFor('navigation/default').set('canCreateTopic', model.get('can_create_topic'));
},
resetController(controller, isExiting) {
if (isExiting) {
controller.setProperties({ order: "default", ascending: false });
}
},
renderTemplate() {
this.render('navigation/default', { outlet: 'navigation-bar' });
this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' });

View File

@ -5,7 +5,7 @@
import OpenComposer from "discourse/mixins/open-composer";
import { scrollTop } from "discourse/mixins/scroll-top";
const DiscoveryRoute = Discourse.Route.extend(OpenComposer, {
export default Discourse.Route.extend(OpenComposer, {
redirect() {
return this.redirectIfLoginRequired();
},
@ -46,9 +46,16 @@ const DiscoveryRoute = Discourse.Route.extend(OpenComposer, {
createTopic() {
this.openComposer(this.controllerFor("discovery/topics"));
},
dismissReadTopics(dismissTopics) {
var operationType = dismissTopics ? "topics" : "posts";
this.controllerFor("discovery/topics").send('dismissRead', operationType);
},
dismissRead(operationType) {
this.controllerFor("discovery/topics").send('dismissRead', operationType);
}
}
});
export default DiscoveryRoute;

View File

@ -43,7 +43,7 @@ const TopicRoute = Discourse.Route.extend({
showFlags(model) {
showModal('flag', { model });
this.controllerFor('flag').setProperties({ selected: null });
this.controllerFor('flag').setProperties({ selected: null, flagTopic: false });
},
showFlagTopic(model) {

View File

@ -33,6 +33,12 @@ export default Discourse.Route.extend({
}
},
beforeModel() {
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
this.replaceWith("discovery");
}
},
model(params) {
// If we're viewing the currently logged in user, return that object instead
const currentUser = this.currentUser;

View File

@ -23,6 +23,12 @@ export default Discourse.Route.extend({
}
},
beforeModel() {
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
this.replaceWith("discovery");
}
},
model(params) {
// If we refresh via `refreshModel` set the old model to loading
this._params = params;

View File

@ -0,0 +1,7 @@
{{yield}}
<div class='controls'>
{{d-button class="btn-primary" label="composer.modal_ok" action="ok"}}
{{d-button class="btn-danger" label="composer.modal_cancel" action="cancel"}}
</div>

View File

@ -0,0 +1,32 @@
<div class='d-editor-overlay hidden'></div>
<div class='d-editor-modals'>
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}
<h3>{{i18n "composer.link_dialog_title"}}</h3>
{{text-field value=link placeholderKey="composer.link_placeholder"}}
{{/d-editor-modal}}
</div>
<div class='d-editor-container'>
<div class='d-editor-button-bar'>
{{d-button action="bold" icon="bold" class="bold"}}
{{d-button action="italic" icon="italic" class="italic"}}
<div class='d-editor-spacer'></div>
{{d-button action="showLinkModal" icon="link" class="link"}}
{{d-button action="quote" icon="quote-right" class="quote"}}
{{d-button action="code" icon="code" class="code"}}
<div class='d-editor-spacer'></div>
{{d-button action="bullet" icon="list-ul" class="bullet"}}
{{d-button action="list" icon="list-ol" class="list"}}
{{d-button action="heading" icon="font" class="heading"}}
{{d-button action="rule" icon="minus" class="rule"}}
{{#if siteSettings.enable_emoji}}
{{d-button action="emoji" icon="smile-o" class="emoji"}}
{{/if}}
</div>
{{textarea value=value class="d-editor-input"}}
<div class="d-editor-preview {{unless preview 'hidden'}}">
{{{preview}}}
</div>
</div>

View File

@ -1,2 +1,2 @@
<label>{{i18n 'category.topic_template'}}</label>
{{pagedown-editor value=category.topic_template}}
{{d-editor value=category.topic_template}}

View File

@ -57,7 +57,7 @@
<li>{{d-link route="badges" class="badge-link" label="badges.title"}}</li>
{{/if}}
{{#if siteSettings.enable_user_directory}}
{{#if showUserDirectoryLink}}
<li>{{d-link route="users" class="user-directory-link" label="directory.title"}}</li>
{{/if}}

View File

@ -1,18 +1,15 @@
<a href="{{unbound linkUrl}}" data-auto-route="true">
{{#if showSmallLogo}}
{{#if smallLogoUrl}}
<span class="valign-helper"></span>
<img class="logo-small" src="{{unbound smallLogoUrl}}" width="33" height="33">
{{else}}
<i class="fa fa-home"></i>
{{/if}}
{{else}}
{{#if showMobileLogo}}
<span class="valign-helper"></span>
<img id="site-logo" class="logo-big" src="{{unbound mobileBigLogoUrl}}" alt="{{unbound title}}">
{{else}}
{{#if bigLogoUrl}}
<span class="valign-helper"></span>
<img id="site-logo" class="logo-big" src="{{unbound bigLogoUrl}}" alt="{{unbound title}}">
{{else}}
<h2 id="site-text-logo" class="text-logo">{{unbound title}}</h2>

View File

@ -1,3 +0,0 @@
<div class='wmd-button-bar'></div>
{{textarea value=value class="wmd-input"}}
<div class="wmd-preview preview {{unless value 'hidden'}}"></div>

View File

@ -7,7 +7,6 @@
{{else}}
<p>{{replace-emoji (i18n "signup_cta.intro")}}</p>
<p>{{replace-emoji (i18n "signup_cta.value_prop")}}</p>
<p>{{signupMethodsTranslated}}</p>
<div class="buttons">
{{d-button action="showCreateAccount" label="signup_cta.sign_up" icon="check" class="btn-primary"}}

View File

@ -5,8 +5,7 @@
{{#if showDismissAtTop}}
<div class="row">
{{#if showDismissRead}}
<button title="{{i18n 'topics.bulk.dismiss_topics_tooltip'}}" id='dismiss-topics-top' class='btn dismiss-read' {{action "dismissRead" "topics"}}>{{i18n 'topics.bulk.dismiss_topics'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_posts_tooltip'}}" id='dismiss-posts-top' class='btn dismiss-read' {{action "dismissRead" "posts"}}>{{i18n 'topics.bulk.dismiss_posts'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_tooltip'}}" id='dismiss-topics-top' class='btn dismiss-read' {{action "dismissReadPosts"}}>{{i18n 'topics.bulk.dismiss_button'}}</button>
{{/if}}
{{#if showResetNew}}
<button id='dismiss-new-top' class='btn dismiss-read' {{action "resetNew"}}>{{i18n 'topics.bulk.dismiss_new'}}</button>
@ -55,8 +54,7 @@
{{conditional-loading-spinner condition=model.loadingMore}}
{{#if allLoaded}}
{{#if showDismissRead}}
<button title="{{i18n 'topics.bulk.dismiss_topics_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissRead" "topics"}}>{{i18n 'topics.bulk.dismiss_topics'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_posts_tooltip'}}" id='dismiss-posts' class='btn dismiss-read' {{action "dismissRead" "posts"}}>{{i18n 'topics.bulk.dismiss_posts'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissReadPosts"}}>{{i18n 'topics.bulk.dismiss_button'}}</button>
{{/if}}
{{#if showResetNew}}
<button id='dismiss-new' class='btn dismiss-read' {{action "resetNew"}}>{{i18n 'topics.bulk.dismiss_new'}}</button>

View File

@ -22,7 +22,7 @@
mobileAction="fullPageSearch"
loginAction="showLogin"
title="search.title"
href="search"}}
path="/search"}}
{{/header-dropdown}}
{{#header-dropdown iconId="toggle-hamburger-menu"

View File

@ -25,8 +25,7 @@
{{conditional-loading-spinner condition=model.loadingMore}}
{{#if allLoaded}}
{{#if showDismissRead}}
<button title="{{i18n 'topics.bulk.dismiss_topics_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissRead" "topics"}}>{{i18n 'topics.bulk.dismiss_topics'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_posts_tooltip'}}" id='dismiss-posts' class='btn dismiss-read' {{action "dismissRead" "posts"}}>{{i18n 'topics.bulk.dismiss_posts'}}</button>
<button title="{{i18n 'topics.bulk.dismiss_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissReadPosts"}}>{{i18n 'topics.bulk.dismiss_button'}}</button>
{{/if}}
{{#if showResetNew}}
<button id='dismiss-new' class='btn dismiss-read' {{action "resetNew"}}>{{i18n 'topics.bulk.dismiss_new'}}</button>

View File

@ -1,28 +1,35 @@
<td>
<div class='main-link'>
{{raw "topic-status" topic=content}}
{{topic-link content}}
{{raw "list/topic-excerpt" topic=content}}
</div>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}}
</div>
<div class="clearfix"></div>
<div class='pull-left'>
<a href="{{content.lastPostUrl}}">{{avatar topic.lastPoster imageSize="large"}}</a>
</div>
<div class="topic-item-stats clearfix">
{{#unless controller.hideCategory}}
<div class='category'>
{{category-link content.category}}
<div class='right'>
<div class='main-link'>
{{raw "topic-status" topic=content}}
{{topic-link content}}
{{raw "list/topic-excerpt" topic=content}}
</div>
{{/unless}}
{{plugin-outlet "topic-list-tags"}}
<div class="pull-right">
<div class='num activity last'>
{{raw "list/activity-column" topic=content tagName="span" class="age"}}
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}}
</div>
<div class="topic-item-stats clearfix">
{{#unless controller.hideCategory}}
<div class='category'>
{{category-link content.category}}
</div>
{{/unless}}
{{plugin-outlet "topic-list-tags"}}
<div class="pull-right">
<div class='num activity last'>
<span class="age activity" title="{{content.bumpedAtTitle}}"><a href="{{content.lastPostUrl}}">{{format-date content.bumpedAt format="tiny" noTitle="true"}}</a></span>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="clearfix"></div>
</div>
</td>

View File

@ -9,7 +9,7 @@
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
</div>
{{#if allowImageUpload}}
{{#if allowAvatarUpload}}
<div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
<label class="radio" for="uploaded_avatar">

View File

@ -1,6 +1,6 @@
{{#each row in buttonRows}}
{{#each buttonRows as |row|}}
<p>
{{#each button in row}}
{{#each row as |button|}}
{{d-button action=button.action label=button.label}}
{{/each}}
</p>

View File

@ -35,17 +35,20 @@
</tr>
{{/if}}
<tr class="input">
<td style="width:80px" class="label"><label for='new-account-name'>{{i18n 'user.name.title'}}</label></td>
<td style="width:496px">
{{text-field value=accountName id="new-account-name"}}
&nbsp;{{input-tip validation=nameValidation}}
{{#if siteSettings.enable_names}}
<tr class="input">
<td style="width:80px" class="label">
<label for='new-account-name'>{{i18n 'user.name.title'}}</label>
</td>
</tr>
<tr class="instructions">
<td style="width:496px">
{{text-field value=accountName id="new-account-name"}}&nbsp;{{input-tip validation=nameValidation}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td><label>{{nameInstructions}}</label></td>
</tr>
</tr>
{{/if}}
{{#if passwordRequired}}
<tr class="input">

View File

@ -0,0 +1,9 @@
<div class="modal-body">
<p>
{{preference-checkbox labelKey="topics.bulk.also_dismiss_topics" checked=dismissTopics}}
</p>
</div>
<div class="modal-footer">
<button class='btn btn-primary' {{action "dismissReadTopics" dismissTopics}}>{{i18n 'topics.bulk.dismiss'}}</button>
</div>

View File

@ -35,7 +35,7 @@
<div class='body'>
{{#if ctrl.editing}}
{{pagedown-editor value=ctrl.buffered.raw}}
{{d-editor value=ctrl.buffered.raw}}
{{else}}
{{{cook-text ctrl.post.raw}}}
{{/if}}

View File

@ -9,7 +9,7 @@
<div class="control-group">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls">
{{pagedown-editor value=model.bio_raw}}
{{d-editor value=model.bio_raw}}
</div>
</div>

View File

@ -90,17 +90,13 @@
<div class="controls">
{{! we want the "huge" version even though we're downsizing it to "large" in CSS }}
{{bound-avatar model "huge"}}
{{#if allowAvatarUpload}}
{{#unless siteSettings.sso_overrides_avatar}}
{{d-button action="showAvatarSelector" class="pad-left" icon="pencil"}}
{{else}}
{{#unless ssoOverridesAvatar}}
<a href="//gravatar.com/emails" target="_blank" title="{{i18n 'user.change_avatar.gravatar_title'}}" class="btn no-text">{{fa-icon "pencil"}}</a>
{{/unless}}
{{/if}}
{{/unless}}
</div>
</div>
{{#if allowBackgrounds}}
{{#if siteSettings.allow_profile_backgrounds}}
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_profile_background.title'}}</label>
<div class="controls">
@ -122,7 +118,7 @@
</div>
{{/if}}
{{#if allowUserLocale}}
{{#if siteSettings.allow_user_locale}}
<div class="control-group pref-locale">
<label class="control-label">{{i18n 'user.locale.title'}}</label>
<div class="controls">
@ -137,13 +133,14 @@
<div class="control-group pref-bio">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls bio-composer">
{{pagedown-editor value=model.bio_raw}}
{{d-editor value=model.bio_raw}}
</div>
</div>
{{#each uf in userFields}}
{{user-field field=uf.field value=uf.value}}
{{/each}}
<div class='clearfix'></div>
<div class="control-group pref-location">
<label class="control-label">{{i18n 'user.location'}}</label>
@ -212,7 +209,7 @@
{{preference-checkbox labelKey="user.enable_quoting" checked=model.enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.dynamic_favicon}}
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.disable_jump_reply}}
{{#unless editHistoryVisible}}
{{#unless siteSettings.edit_history_visible_to_public}}
{{preference-checkbox labelKey="user.edit_history_public" checked=model.edit_history_public}}
{{/unless}}
@ -238,6 +235,13 @@
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
</div>
<div class="control-group topics">
<label class="control-label">{{i18n 'categories.topics'}}</label>
<div class="controls topic-controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
</div>
</div>
<div class="control-group muting">
<label class="control-label">{{i18n 'user.users'}}</label>
<div class="controls category-controls">

View File

@ -34,27 +34,27 @@
<ul>
{{#if model.can_send_private_message_to_user}}
<li>
<a class='btn btn-primary right' {{action "composePrivateMessage" model}}>
<a class='btn btn-primary' {{action "composePrivateMessage" model}}>
{{fa-icon "envelope"}}
{{i18n 'user.private_message'}}
</a>
</li>
{{/if}}
{{#if viewingSelf}}
<li><a {{action "logout"}} href class='btn btn-danger right'>{{fa-icon "sign-out"}}{{i18n 'user.log_out'}}</a></li>
<li><a {{action "logout"}} href class='btn btn-danger'>{{fa-icon "sign-out"}}{{i18n 'user.log_out'}}</a></li>
{{/if}}
{{#if currentUser.staff}}
<li><a href={{model.adminPath}} class='btn right'>{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
<li><a href={{model.adminPath}} class="btn">{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
{{/if}}
{{#if model.can_edit}}
<li>{{#link-to 'preferences' class="btn right"}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
<li>{{#link-to 'preferences' class="btn"}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
{{#if canInviteToForum}}
<li>{{#link-to 'userInvited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
<li>{{#link-to 'userInvited' class="btn"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
{{/if}}
{{#if collapsedInfo}}
{{#if viewingSelf}}
<li><a {{action "expandProfile"}} href class="btn right">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li>
<li><a {{action "expandProfile"}} href class="btn">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li>
{{/if}}
{{/if}}
</ul>
@ -122,9 +122,9 @@
{{/if}}
<dt>{{i18n 'views'}}</dt><dd>{{model.profile_view_count}}</dd>
{{#if model.invited_by}}
<dt>{{i18n 'user.invited_by'}}</dt><dd>{{#link-to 'user' model.invited_by}}{{model.invited_by.username}}{{/link-to}}</dd>
<dt class="invited-by">{{i18n 'user.invited_by'}}</dt><dd class="invited-by">{{#link-to 'user' model.invited_by}}{{model.invited_by.username}}{{/link-to}}</dd>
{{/if}}
<dt>{{i18n 'user.trust_level'}}</dt><dd>{{model.trustLevel.name}}</dd>
<dt class="trust-level">{{i18n 'user.trust_level'}}</dt><dd class="trust-level">{{model.trustLevel.name}}</dd>
{{#if canCheckEmails}}
<dt>{{i18n 'user.email.title'}}</dt>
<dd title={{model.email}}>

View File

@ -185,7 +185,9 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
_applyEmojiAutocomplete() {
if (!this.siteSettings.enable_emoji) { return; }
const template = this.container.lookup('template:emoji-selector-autocomplete.raw');
const container = this.container;
const template = container.lookup('template:emoji-selector-autocomplete.raw');
const controller = this.get('controller');
this.$('.wmd-input').autocomplete({
template: template,
@ -195,7 +197,12 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
if (v.code) {
return `${v.code}:`;
} else {
showSelector({ skipPrefix: true });
showSelector({
container,
onSelect(title) {
controller.appendTextAtCursor(title + ':', {space: false});
}
});
return "";
}
},

View File

@ -26,5 +26,6 @@ export default ContainerView.extend({
if (this.get('topic.details.can_create_post')) {
this.attachViewClass('reply-button');
}
this.trigger('additionalButtons', this);
}
});

View File

@ -27,9 +27,9 @@ export default ModalBodyView.extend({
},
tip: function() {
const source = this.get("controller.local") ? "local" : "remote",
opts = { authorized_extensions: Discourse.Utilities.authorizedExtensions() };
return uploadTranslate(source + "_tip", opts);
const source = this.get("controller.local") ? "local" : "remote";
const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`;
return uploadTranslate(source + "_tip", { authorized_extensions });
}.property("controller.local"),
hint: function() {

View File

@ -75,6 +75,7 @@
//= require ./discourse/views/header
//= require ./discourse/dialects/dialect
//= require ./discourse/lib/emoji/emoji
//= require ./discourse/lib/emoji/emoji-groups
//= require ./discourse/lib/emoji/emoji-toolbar
//= require ./discourse/views/composer
//= require ./discourse/lib/show-modal

View File

@ -10,4 +10,5 @@
@import "common/topic-entrance";
@import "common/printer-friendly";
@import "common/base/*";
@import "common/d-editor";
@import "vendor/pikaday";

View File

@ -213,6 +213,19 @@ td.flaggers td {
display: inline-block;
margin-right: 5px;
}
#last-seen {
float: none;
}
.ac-wrap {
display: inline-block;
vertical-align: middle;
padding: 0;
}
}
.paste-users {
width: 400px;
height: 150px;
}
.groups, .badges {
@ -1015,6 +1028,11 @@ table.api-keys {
}
}
.groups-bulk {
.control {
margin-bottom: 1em;
}
}
.commits-widget {
border: solid 1px dark-light-diff($primary, $secondary, 90%, -60%);
@ -1165,10 +1183,6 @@ table.api-keys {
margin-top: 10px;
}
.pagedown-editor {
width: 98%;
}
textarea.plain {
width: 98%;
height: 200px;
@ -1478,9 +1492,6 @@ and (max-width : 500px) {
.content-editor {
width: 100%;
.pagedown-editor {
box-sizing: border-box;
}
}
div.ac-wrap {

View File

@ -153,24 +153,6 @@ body {
resize: none;
}
.pagedown-editor {
width: 540px;
background-color: $secondary;
padding: 0 10px 13px 10px;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
.preview {
margin-top: 8px;
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
padding: 8px 8px 0 8px;
p {
margin: 0 0 10px 0;
}
}
.preview.hidden {
display: none;
}
}
.avatar-wrapper {
background-color: $secondary;
display: inline-block;

View File

@ -17,9 +17,10 @@ body img.emoji {
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));
}
.emoji-page td {
table.emoji-page td {
border: 1px solid transparent;
background-color: dark-light-choose(white, $secondary);
padding: 0 !important;
}
.emoji-page a {

View File

@ -19,12 +19,6 @@
float: left;
}
.valign-helper {
display: inline-block;
height: 100%;
vertical-align: middle;
}
#site-logo {
max-height: 40px;
}

View File

@ -103,10 +103,6 @@
.modal.edit-category-modal {
.modal-body {
.pagedown-editor {
width: 98%;
}
textarea {
height: 10em;
}

View File

@ -1,6 +1,6 @@
.uploaded-image-preview {
background: $primary center;
background-size: cover;
background: $primary center center;
}
.image-uploader.no-repeat {

View File

@ -0,0 +1,94 @@
.d-editor {
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
.d-editor-container {
padding: 0 10px 13px 10px;
}
.d-editor-overlay {
position: absolute;
background-color: black;
opacity: 0.8;
}
.d-editor-modals {
position: absolute;
}
.d-editor .d-editor-modal {
min-width: 400px;
position: absolute;
background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
padding: 1em;
top: 50px;
input {
width: 98%;
}
h3 {
margin-bottom: 0.5em;
}
}
.d-editor-button-bar {
margin: 5px;
padding: 0;
height: 20px;
overflow: hidden;
button {
background-color: transparent;
padding: 2px 4px;
float: left;
margin-right: 6px;
}
}
.d-editor-spacer {
width: 1px;
height: 20px;
margin-right: 8px;
margin-left: 5px;
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
display: inline-block;
float: left;
}
.d-editor-input {
color: $primary;
width: 98%;
height: 200px;
&:disabled {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
}
.d-editor-preview {
color: $primary;
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
overflow: auto;
visibility: visible;
cursor: default;
margin-top: 8px;
padding: 8px 8px 0 8px;
video {
max-width: 100%;
max-height: 500px;
height: auto;
}
audio {
max-width: 100%;
}
&.hidden {
width: 0;
visibility: hidden;
}
}
.d-editor-preview > *:first-child {
margin-top: 0;
}

View File

@ -84,7 +84,7 @@ body {
margin: 0;
i {
font-size: 1.071em;
}
}
}
i.fa-envelope {

View File

@ -298,6 +298,7 @@ button.dismiss-read {
@media all
and (max-width : 850px) {
// slightly smaller font, tighten spacing on nav pills
.nav-pills {
> li > a {
font-size: 1em;
@ -305,60 +306,32 @@ and (max-width : 850px) {
}
}
.list-controls {
.btn {
font-size: 1em
}
.category-dropdown-menu {
min-width: 139px;
}
a.badge-category {
font-size: 1em;
}
}
.topic-list {
.categories td.category {
padding-left: 10px;
}
// tighter table header spacing
th:first-of-type {
padding: 12px 5px;
}
th {
.btn .fa {
margin-right: 2px;
}
}
// smaller table cell font and cell spacing
th, td {
padding: 12px 2px;
font-size: 0.929em;
}
.star {
padding: 12px 5px;
width: auto;
}
.main-link {
font-size: 1.071em;
padding: 12px 8px 12px 0px;
}
.likes {
width: auto;
}
.category {
min-width: 0;
padding: 0;
}
.topic-excerpt {
padding-right: 20px;
}
th.posters {
text-align: center;
}
// suppress views column
th.views {
display: none;
}
td.views {
display: none;
}
// show only the first poster
.posters {
min-width: 0;
width: 50px;
@ -368,6 +341,6 @@ and (max-width : 850px) {
a.latest {
padding: 0 8px;
}
}
}
}
}

View File

@ -37,6 +37,6 @@
.image-upload-controls {
padding: 10px;
label.btn {
padding: 7px 10px 5px 10px;
padding: 6px 10px;
}
}

View File

@ -39,14 +39,6 @@
}
}
.pagedown-editor {
width: 450px;
textarea {
width: 440px;
}
}
.bio-composer #wmd-quote-post {
display: none;
}

View File

@ -5,6 +5,8 @@
// there are (n) new or updated topics, click to show
.alert.alert-info {
margin: 0;
margin-bottom: -3px;
margin-top: -5px;
padding: 15px;
font-size: 1.1em;
}

View File

@ -3,7 +3,9 @@
// --------------------------------------------------
#banner {
margin: 10px;
// go full width on mobile, by extending into the 10px wrap
// borders on left and right
margin: 0 -10px;
@media all and (max-height: 499px) {
max-height: 100px;

View File

@ -64,8 +64,7 @@ blockquote {
}
.topic-statuses {
display: inline-block;
vertical-align: middle;
float: left;
.topic-status {
i {
color: dark-light-diff($secondary, $primary, 40%, -20%);

View File

@ -5,12 +5,12 @@
.d-header {
#site-logo {
max-width: 155px;
max-width: 130px;
}
// some protection for text-only site titles
.title {
max-width: 160px;
max-width: 130px;
height: 39px;
overflow: hidden;
padding: 0;

View File

@ -63,6 +63,10 @@
.topic-list {
.right {
margin-left: 55px;
}
> tbody > tr {
&.highlighted {
background-color: dark-light-choose(scale-color($tertiary, $lightness: 85%), scale-color($tertiary, $lightness: -55%));
@ -120,7 +124,9 @@
.age {
white-space: nowrap;
a {
padding: 15px 10px 15px 5px;
// let's make all ages dim on mobile so we're not
// overwhelming people with info about each topic
color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important;
}
}
}
@ -140,6 +146,7 @@
.posts {
width: 10%;
vertical-align: top;
}
.age {
@ -236,10 +243,15 @@ tr.category-topic-link {
font-size: 110%;
}
.category-topic-link .num {
white-space: nowrap;
.number {
font-size: 1.071em;
.category-topic-link {
.num {
white-space: nowrap;
.number {
font-size: 1.071em;
}
}
.topic-excerpt {
width: 110%;
}
}
@ -404,4 +416,10 @@ td .main-link {
font-size: 1.071em;
padding-top: 2px;
}
// so the topic excerpt is full width
// as the containing div is 80%
.topic-excerpt {
padding-right: 0;
width: 120%;
}
}

View File

@ -67,10 +67,6 @@
display: none;
}
.pagedown-editor {
width: 100%;
}
textarea {width: 100%;}
}
@ -260,8 +256,9 @@
}
.details {
padding: 15px 15px 4px 15px;
padding: 15px 10px 4px 10px;
background-color: dark-light-choose(rgba($primary, .9), rgba($secondary, .9));
opacity: 0.8;
h1 {
font-size: 2.143em;
@ -295,7 +292,6 @@
img.avatar {
float: left;
margin-right: 15px;
}
.suspended {

View File

@ -34,7 +34,9 @@ class Admin::EmailController < Admin::AdminController
def preview_digest
params.require(:last_seen_at)
renderer = Email::Renderer.new(UserNotifications.digest(current_user, since: params[:last_seen_at]))
params.require(:username)
user = User.find_by_username(params[:username])
renderer = Email::Renderer.new(UserNotifications.digest(user, since: params[:last_seen_at]))
render json: MultiJson.dump(html_content: renderer.html, text_content: renderer.text)
end

View File

@ -19,6 +19,42 @@ class Admin::GroupsController < Admin::AdminController
render nothing: true
end
def bulk
render nothing: true
end
def bulk_perform
group = Group.find(params[:group_id].to_i)
if group.present?
users = (params[:users] || []).map {|u| u.downcase}
user_ids = User.where("username_lower in (:users) OR email IN (:users)", users: users).pluck(:id)
if user_ids.present?
Group.exec_sql("INSERT INTO group_users
(group_id, user_id, created_at, updated_at)
SELECT #{group.id},
u.id,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM users AS u
WHERE u.id IN (#{user_ids.join(', ')})
AND NOT EXISTS(SELECT 1 FROM group_users AS gu
WHERE gu.user_id = u.id AND
gu.group_id = #{group.id})")
if group.primary_group?
User.where(id: user_ids).update_all(primary_group_id: group.id)
end
if group.title.present?
User.where(id: user_ids).update_all(title: group.title)
end
end
end
render json: success_json
end
def create
group = Group.new

View File

@ -121,7 +121,10 @@ class Admin::UsersController < Admin::AdminController
def add_group
group = Group.find(params[:group_id].to_i)
return render_json_error group unless group && !group.automatic
group.users << @user
# We don't care about duplicate group assignment
group.users << @user rescue ActiveRecord::RecordNotUnique
render nothing: true
end

View File

@ -86,13 +86,11 @@ class ApplicationController < ActionController::Base
rescue_from Discourse::NotLoggedIn do |e|
raise e if Rails.env.test?
if (request.format && request.format.json?) || request.xhr? || !request.get?
rescue_discourse_actions(:not_logged_in, 403, true)
else
redirect_to path("/")
rescue_discourse_actions(:not_found, 404)
end
end
rescue_from Discourse::NotFound do
@ -243,7 +241,7 @@ class ApplicationController < ActionController::Base
end
def can_cache_content?
!current_user.present?
current_user.blank? && flash[:authentication_data].blank?
end
# Our custom cache method

View File

@ -16,7 +16,12 @@ class PostsController < ApplicationController
end
def markdown_num
markdown Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i)
if params[:revision].present?
post_revision = find_post_revision_from_topic_id
render text: post_revision.modifications[:raw].last, content_type: 'text/plain'
else
markdown Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i)
end
end
def markdown(post)
@ -403,6 +408,22 @@ class PostsController < ApplicationController
post_revision
end
def find_post_revision_from_topic_id
post = Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i)
raise Discourse::NotFound unless guardian.can_see?(post)
revision = params[:revision].to_i
raise Discourse::NotFound if revision < 2
post_revision = PostRevision.find_by(post_id: post.id, number: revision)
raise Discourse::NotFound unless post_revision
post_revision.post = post
guardian.ensure_can_see!(post_revision)
post_revision
end
private
def user_posts(guardian, user_id, opts)

View File

@ -37,7 +37,11 @@ class SessionController < ApplicationController
sso.external_id = current_user.id.to_s
sso.admin = current_user.admin?
sso.moderator = current_user.moderator?
redirect_to sso.to_url(sso.return_sso_url)
if request.xhr?
cookies[:sso_destination_url] = sso.to_url(sso.return_sso_url)
else
redirect_to sso.to_url(sso.return_sso_url)
end
else
session[:sso_payload] = request.query_string
redirect_to path('/login')
@ -266,9 +270,8 @@ class SessionController < ApplicationController
if payload = session.delete(:sso_payload)
sso_provider(payload)
else
render_serialized(user, UserSerializer)
end
render_serialized(user, UserSerializer)
end
end

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