Version bump

This commit is contained in:
Neil Lalonde 2015-10-19 17:34:56 -04:00
commit 548c18dd51
251 changed files with 3559 additions and 2072 deletions

View File

@ -35,7 +35,8 @@ gem 'barber'
gem 'babel-transpiler'
gem 'message_bus'
gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
gem 'rails_multisite'
gem 'fast_xs'
@ -121,7 +122,7 @@ group :test, :development do
gem 'rspec-given'
gem 'pry-nav'
gem 'spork-rails'
gem 'byebug'
gem 'byebug', require: ENV['RM_INFO'].nil?
end
group :development do

View File

@ -1,8 +1,3 @@
PATH
remote: vendor/gems/rails_multisite
specs:
rails_multisite (0.0.1)
GEM
remote: https://rubygems.org/
specs:
@ -47,12 +42,12 @@ GEM
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
arel (6.0.3)
aws-sdk (2.1.23)
aws-sdk-resources (= 2.1.23)
aws-sdk-core (2.1.23)
aws-sdk (2.1.29)
aws-sdk-resources (= 2.1.29)
aws-sdk-core (2.1.29)
jmespath (~> 1.0)
aws-sdk-resources (2.1.23)
aws-sdk-core (= 2.1.23)
aws-sdk-resources (2.1.29)
aws-sdk-core (= 2.1.29)
babel-source (5.8.19)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
@ -304,6 +299,7 @@ GEM
loofah (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
rails_multisite (1.0.2)
railties (4.2.4)
actionpack (= 4.2.4)
activesupport (= 4.2.4)
@ -504,7 +500,7 @@ DEPENDENCIES
rack-protection
rails (~> 4.2)
rails-observers
rails_multisite!
rails_multisite
rake
rb-fsevent
rb-inotify (~> 0.9)

View File

@ -11,8 +11,8 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
## Screenshots
<a href="https://bbs.boingboing.net"><img src="https://www.discourse.org/faq/14/boing-boing-discourse.png" width="720px"></a>
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/14/new-relic-discourse.png" width="720px"></a>
<a href="https://discuss.newrelic.com/"><img src="https://www.discourse.org/faq/14/how-to-geek-discourse.png" width="720px"></a>
<a href="https://discuss.newrelic.com/"><img src="https://www.discourse.org/faq/14/new-relic-discourse.png" width="720px"></a>
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/14/how-to-geek-discourse.png" width="720px"></a>
<a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/14/turtle-rock-discourse.jpg" width="720px"></a>
<a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/14/nexus-7-mobile-discourse.png" alt="Atom" width="410px"></a> &nbsp;

View File

@ -228,7 +228,7 @@ const AdminUser = Discourse.User.extend({
type: 'POST',
data: { username_or_email: this.get('username') }
}).then(function() {
document.location = Discourse.getURL("/");
document.location = Discourse.BaseUri;
}).catch(function(e) {
if (e.status === 404) {
bootbox.alert(I18n.t('admin.impersonate.not_found'));

View File

@ -1,11 +1,11 @@
{{#if user_deleted}}
{{#if model.user_deleted}}
<button title="{{i18n 'admin.flags.agree_flag_restore_post_title'}}" {{action "agreeFlagRestorePost"}} class="btn"><i class="fa fa-thumbs-o-up"></i><i class="fa fa-eye"></i>{{i18n 'admin.flags.agree_flag_restore_post'}}</button>
{{else}}
{{#unless postHidden}}
{{#unless model.postHidden}}
<button title="{{i18n 'admin.flags.agree_flag_hide_post_title'}}" {{action "agreeFlagHidePost"}} class="btn"><i class="fa fa-thumbs-o-up"></i><i class="fa fa-eye-slash"></i>{{i18n 'admin.flags.agree_flag_hide_post'}}</button>
{{/unless}}
{{/if}}
<button title="{{i18n 'admin.flags.agree_flag_title'}}" {{action "agreeFlagKeepPost"}} class="btn"><i class="fa fa-thumbs-o-up"></i>{{i18n 'admin.flags.agree_flag'}}</button>
{{#if canDeleteAsSpammer}}
{{#if model.canDeleteAsSpammer}}
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
{{/if}}

View File

@ -1,5 +1,5 @@
<button title="{{i18n 'admin.flags.delete_post_defer_flag_title'}}" {{action "deletePostDeferFlag"}} class="btn"><i class="fa fa-trash-o"></i><i class="fa fa-external-link"></i>{{i18n 'admin.flags.delete_post_defer_flag'}}</button>
<button title="{{i18n 'admin.flags.delete_post_agree_flag_title'}}" {{action "deletePostAgreeFlag"}} class="btn"><i class="fa fa-trash-o"></i><i class="fa fa-thumbs-o-up"></i>{{i18n 'admin.flags.delete_post_agree_flag'}}</button>
{{#if canDeleteAsSpammer}}
{{#if model.canDeleteAsSpammer}}
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
{{/if}}

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

@ -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('willDestoryElement')
_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 || "", {});
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

@ -1,22 +1,24 @@
import computed from 'ember-addons/ember-computed-decorators';
import KeyValueStore from 'discourse/lib/key-value-store';
const keyValueStore = new KeyValueStore("discourse_desktop_notifications_");
export default Ember.Component.extend({
classNames: ['controls'],
@computed
notificationsPermission() {
if (this.get('isNotSupported')) return '';
return Notification.permission;
@computed("isNotSupported")
notificationsPermission(isNotSupported) {
return isNotSupported ? "" : Notification.permission;
},
@computed
notificationsDisabled: {
set(value) {
localStorage.setItem('notifications-disabled', value);
return localStorage.getItem('notifications-disabled');
keyValueStore.setItem('notifications-disabled', value);
return keyValueStore.getItem('notifications-disabled');
},
get() {
return localStorage.getItem('notifications-disabled');
return keyValueStore.getItem('notifications-disabled');
}
},
@ -25,44 +27,40 @@ export default Ember.Component.extend({
return typeof window.Notification === "undefined";
},
isDefaultPermission: function() {
if (this.get('isNotSupported')) return false;
@computed("isNotSupported", "notificationsPermission")
isDefaultPermission(isNotSupported, notificationsPermission) {
return isNotSupported ? false : notificationsPermission === "default";
},
return Notification.permission === "default";
}.property('isNotSupported', 'notificationsPermission'),
@computed("isNotSupported", "notificationsPermission")
isDeniedPermission(isNotSupported, notificationsPermission) {
return isNotSupported ? false : notificationsPermission === "denied";
},
isDeniedPermission: function() {
if (this.get('isNotSupported')) return false;
@computed("isNotSupported", "notificationsPermission")
isGrantedPermission(isNotSupported, notificationsPermission) {
return isNotSupported ? false : notificationsPermission === "granted";
},
return Notification.permission === "denied";
}.property('isNotSupported', 'notificationsPermission'),
isGrantedPermission: function() {
if (this.get('isNotSupported')) return false;
return Notification.permission === "granted";
}.property('isNotSupported', 'notificationsPermission'),
isEnabled: function() {
if (!this.get('isGrantedPermission')) return false;
return !this.get('notificationsDisabled');
}.property('isGrantedPermission', 'notificationsDisabled'),
@computed("isGrantedPermission", "notificationsDisabled")
isEnabled(isGrantedPermission, notificationsDisabled) {
return isGrantedPermission ? !notificationsDisabled : false;
},
actions: {
requestPermission() {
const self = this;
Notification.requestPermission(function() {
self.propertyDidChange('notificationsPermission');
});
Notification.requestPermission(() => this.propertyDidChange('notificationsPermission'));
},
recheckPermission() {
this.propertyDidChange('notificationsPermission');
},
turnoff() {
this.set('notificationsDisabled', 'disabled');
this.propertyDidChange('notificationsPermission');
},
turnon() {
this.set('notificationsDisabled', '');
this.propertyDidChange('notificationsPermission');

View File

@ -133,8 +133,7 @@ export default Ember.Component.extend({
this._resizeInterval = setInterval(() => {
Ember.run(() => {
const $panelBodyContents = this.$('.panel-body-contents');
if ($panelBodyContents.length) {
if ($panelBodyContents && $panelBodyContents.length) {
const contentHeight = parseInt($panelBodyContents.height());
if (contentHeight !== this._lastHeight) { this.performLayout(); }
this._lastHeight = contentHeight;

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

@ -364,7 +364,9 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
rebakePostIcon = iconHTML('cog'),
rebakePostText = I18n.t('post.controls.rebake'),
unhidePostIcon = iconHTML('eye'),
unhidePostText = I18n.t('post.controls.unhide');
unhidePostText = I18n.t('post.controls.unhide'),
changePostOwnerIcon = iconHTML('user'),
changePostOwnerText = I18n.t('post.controls.change_owner');
const html = '<div class="post-admin-menu popup-menu">' +
'<h3>' + I18n.t('admin_title') + '</h3>' +
@ -373,6 +375,7 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
(Discourse.User.currentProp('admin') ? '<li class="btn" data-action="changePostOwner">' + changePostOwnerIcon + changePostOwnerText + '</li>' : '') +
'</ul>' +
'</div>';
@ -404,6 +407,10 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
this.sendAction("unhidePost", this.get("post"));
},
clickChangePostOwner() {
this.sendAction("changePostOwner", this.get("post"));
},
buttonForShowMoreActions() {
return new Button('showMoreActions', 'show_more', 'ellipsis-h');
},

View File

@ -42,7 +42,10 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, {
Discourse.Topic.changeOwners(this.get('topicController.model.id'), saveOpts).then(function() {
// success
self.send('closeModal');
self.get('topicController').send('toggleMultiSelect');
self.get('topicController').send('deselectAll');
if (self.get('topicController.multiSelect')) {
self.get('topicController').send('toggleMultiSelect');
}
Em.run.next(() => { DiscourseURL.routeTo(self.get("topicController.model.url")); });
}, function() {
// failure

View File

@ -12,12 +12,10 @@ export var queryParams = {
// Basic controller options
var controllerOpts = {
needs: ['discovery/topics'],
queryParams: Ember.keys(queryParams)
queryParams: Ember.keys(queryParams),
};
// Aliases for the values
controllerOpts.queryParams.forEach(function(p) {
controllerOpts[p] = Em.computed.alias('controllers.discovery/topics.' + p);
});
controllerOpts.queryParams.forEach(p => controllerOpts[p] = Em.computed.alias(`controllers.discovery/topics.${p}`));
export default Ember.Controller.extend(controllerOpts);

View File

@ -25,6 +25,7 @@ const controllerOpts = {
} else {
this.setProperties({ order: sortBy, ascending: false });
}
this.get('model').refreshSort(sortBy, this.get('ascending'));
},
@ -41,7 +42,7 @@ const controllerOpts = {
refresh() {
const filter = this.get('model.filter');
this.setProperties({ order: 'default', ascending: false });
this.setProperties({ order: "default", ascending: false });
// Don't refresh if we're still loading
if (this.get('controllers.discovery.loading')) { return; }
@ -51,7 +52,7 @@ const controllerOpts = {
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
this.store.findFiltered('topicList', {filter}).then((list) => {
this.store.findFiltered('topicList', {filter}).then(list => {
Discourse.TopicList.hideUniformCategory(list, this.get('category'));
this.setProperties({ model: list });

View File

@ -80,7 +80,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
const shouldRedirectToUrl = self.session.get("shouldRedirectToUrl");
$hidden_login_form.find('input[name=username]').val(self.get('loginName'));
$hidden_login_form.find('input[name=password]').val(self.get('loginPassword'));
if (self.get('loginRequired') && destinationUrl) {
if (destinationUrl) {
// redirect client to the original URL
$.cookie('destination_url', null);
$hidden_login_form.find('input[name=redirect]').val(destinationUrl);
@ -113,21 +113,26 @@ export default Ember.Controller.extend(ModalFunctionality, {
if(customLogin){
customLogin();
} else {
this.set('authenticate', name);
const left = this.get('lastX') - 400;
const top = this.get('lastY') - 200;
var authUrl = Discourse.getURL("/auth/" + name);
if (loginMethod.get("fullScreenLogin")) {
window.location = authUrl;
} else {
this.set('authenticate', name);
const left = this.get('lastX') - 400;
const top = this.get('lastY') - 200;
const height = loginMethod.get("frameHeight") || 400;
const width = loginMethod.get("frameWidth") || 800;
const w = window.open(Discourse.getURL("/auth/" + name), "_blank",
"menubar=no,status=no,height=" + height + ",width=" + width + ",left=" + left + ",top=" + top);
const self = this;
const timer = setInterval(function() {
if(!w || w.closed) {
clearInterval(timer);
self.set('authenticate', null);
}
}, 1000);
const height = loginMethod.get("frameHeight") || 400;
const width = loginMethod.get("frameWidth") || 800;
const w = window.open(authUrl, "_blank",
"menubar=no,status=no,height=" + height + ",width=" + width + ",left=" + left + ",top=" + top);
const self = this;
const timer = setInterval(function() {
if(!w || w.closed) {
clearInterval(timer);
self.set('authenticate', null);
}
}, 1000);
}
}
},

View File

@ -15,7 +15,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
selectedPosts: null,
selectedReplies: null,
queryParams: ['filter', 'username_filters', 'show_deleted'],
loadedAllPosts: false,
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
enteredAt: null,
firstPostExpanded: false,
retrying: false,
@ -36,22 +36,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}
}.observes('model.title', 'category'),
postStreamLoadedAllPostsChanged: function() {
// semantics of loaded all posts are slightly diff at topic level,
// it just means that we "once" loaded all posts, this means we don't
// keep re-rendering the suggested topics when new posts zoom in
let loaded = this.get('model.postStream.loadedAllPosts');
if (loaded) {
this.set('model.loadedTopicId', this.get('model.id'));
} else {
loaded = this.get('model.loadedTopicId') === this.get('model.id');
}
this.set('loadedAllPosts', loaded);
}.observes('model.postStream', 'model.postStream.loadedAllPosts'),
@computed('model.postStream.summary')
show_deleted: {
set(value) {
@ -458,6 +442,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
unhidePost(post) {
post.unhide();
},
changePostOwner(post) {
this.get('selectedPosts').addObject(post);
this.send('changeOwner');
}
},
@ -594,7 +583,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}
case "created": {
postStream.triggerNewPostInStream(data.id);
Discourse.notifyBackgroundCountIncrement();
if (self.get('currentUser.id') !== data.user_id) {
Discourse.notifyBackgroundCountIncrement();
}
return;
}
default: {

View File

@ -135,7 +135,7 @@ function invalidBoundary(args, prev) {
var last = prev[prev.length - 1];
if (typeof last !== "string") { return false; }
if (args.wordBoundary && (last.match(/(\w|\/)$/))) { return true; }
if (args.wordBoundary && (!last.match(/\W$/))) { return true; }
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
if (args.spaceOrTagBoundary && (!last.match(/(\s|\>)$/))) { return true; }
}

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

@ -130,10 +130,13 @@ export default function(options) {
if (options.transformComplete) {
term = options.transformComplete(term);
}
var text = me.val();
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
me.val(text);
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
if (term) {
var text = me.val();
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
me.val(text);
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
}
}
}
closeAutocomplete();
@ -284,7 +287,7 @@ export default function(options) {
if (options.key && e.which === options.key.charCodeAt(0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]);
var prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || /\W/.test(prevChar)) {
if (!prevChar || /[^\w\)\]]/.test(prevChar)) {
completeStart = completeEnd = caretPosition;
updateAutoComplete(options.dataSource(""));
}
@ -338,7 +341,7 @@ export default function(options) {
stopFound = prev === options.key;
if (stopFound) {
prev = me[0].value[c - 1];
if (!prev || /\W/.test(prev)) {
if (!prev || /[^\w\)\]]/.test(prev)) {
completeStart = c;
caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial);

View File

@ -1,5 +1,6 @@
import DiscourseURL from 'discourse/lib/url';
import PageTracker from 'discourse/lib/page-tracker';
import KeyValueStore from 'discourse/lib/key-value-store';
let primaryTab = false;
let liveEnabled = false;
@ -10,6 +11,8 @@ let lastAction = -1;
const focusTrackerKey = "focus-tracker";
const idleThresholdTime = 1000 * 10; // 10 seconds
const keyValueStore = new KeyValueStore("discourse_desktop_notifications_");
// Called from an initializer
function init(messageBus) {
liveEnabled = false;
@ -20,7 +23,7 @@ function init(messageBus) {
}
try {
localStorage.getItem(focusTrackerKey);
keyValueStore.getItem(focusTrackerKey);
} catch (e) {
Em.Logger.info('Discourse desktop notifications are disabled - localStorage denied.');
return;
@ -66,7 +69,7 @@ function setupNotifications() {
window.addEventListener("focus", function() {
if (!primaryTab) {
primaryTab = true;
localStorage.setItem(focusTrackerKey, mbClientId);
keyValueStore.setItem(focusTrackerKey, mbClientId);
}
});
@ -74,7 +77,7 @@ function setupNotifications() {
primaryTab = false;
} else {
primaryTab = true;
localStorage.setItem(focusTrackerKey, mbClientId);
keyValueStore.setItem(focusTrackerKey, mbClientId);
}
if (document) {
@ -95,7 +98,7 @@ function onNotification(data) {
if (!liveEnabled) { return; }
if (!primaryTab) { return; }
if (!isIdle()) { return; }
if (localStorage.getItem('notifications-disabled')) { return; }
if (keyValueStore.getItem('notifications-disabled')) { return; }
const notificationTitle = I18n.t(i18nKey(data.notification_type), {
site_title: Discourse.SiteSettings.title,

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,148 +1,85 @@
// 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"]
}
];
import groups from 'discourse/lib/emoji/emoji-groups';
import KeyValueStore from "discourse/lib/key-value-store";
// scrub groups
groups.forEach(function(group){
group.icons = _.reject(group.icons, function(obj){
return !Discourse.Emoji.exists(obj);
});
});
const keyValueStore = new KeyValueStore("discourse_emojis_");
const EMOJI_USAGE = "emojiUsage";
// export so others can modify
Discourse.Emoji.groups = groups;
const PER_ROW = 12, PER_PAGE = 60;
let ungroupedIcons, recentlyUsedIcons;
var closeSelector = function(){
if (!keyValueStore.getObject(EMOJI_USAGE)) {
keyValueStore.setObject({key: EMOJI_USAGE, value: {}});
}
function closeSelector() {
$('.emoji-modal, .emoji-modal-wrapper').remove();
$('body, textarea').off('keydown.emoji');
};
}
var ungroupedIcons, recentlyUsedIcons;
function initializeUngroupedIcons() {
const groupedIcons = {};
var initializeUngroupedIcons = function(){
ungroupedIcons = [];
var groupedIcons = {};
_.each(groups, function(group){
_.each(group.icons, function(icon){
groupedIcons[icon] = true;
});
groups.forEach(group => {
group.icons.forEach(icon => groupedIcons[icon] = true);
});
var emojis = Discourse.Emoji.list();
_.each(emojis, function(emoji){
if(groupedIcons[emoji] !== true){
ungroupedIcons = [];
const emojis = Discourse.Emoji.list();
emojis.forEach(emoji => {
if (groupedIcons[emoji] !== true) {
ungroupedIcons.push(emoji);
}
});
if(ungroupedIcons.length > 0){
if (ungroupedIcons.length) {
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
};
try {
if (localStorage && !localStorage.emojiUsage) { localStorage.emojiUsage = "{}"; }
} catch(e){
/* localStorage can be disabled, or cookies disabled, do not crash script here
* TODO introduce a global wrapper for dealing with local storage
* */
}
var trackEmojiUsage = function(title){
var recent = JSON.parse(localStorage.emojiUsage);
function trackEmojiUsage(title) {
const recent = keyValueStore.getObject(EMOJI_USAGE);
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
recent[title]["usage"]++;
localStorage.emojiUsage = JSON.stringify(recent);
keyValueStore.setObject({key: EMOJI_USAGE, value: recent});
// 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(JSON.parse(localStorage.emojiUsage));
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";
@ -150,60 +87,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) {
localStorage.emojiPage = page;
localStorage.emojiOffset = offset;
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);
@ -213,39 +138,38 @@ 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>');
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
$('.emoji-modal-wrapper').click(() => closeSelector());
$('.emoji-modal-wrapper').click(function(){
closeSelector();
});
const page = keyValueStore.getInt("emojiPage", 0);
const offset = keyValueStore.getInt("emojiOffset", 0);
var page = parseInt(localStorage.emojiPage) || 0;
var offset = parseInt(localStorage.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

@ -5,13 +5,15 @@ try {
safeLocalStorage = localStorage;
if (localStorage["disableLocalStorage"] === "true") {
safeLocalStorage = null;
} else {
// makes sure we can write to the local storage
safeLocalStorage["safeLocalStorage"] = true;
}
} catch(e){
} catch (e) {
// cookies disabled, we don't care
safeLocalStorage = null;
}
const KeyValueStore = function(ctx) {
this.context = ctx;
};
@ -41,6 +43,10 @@ KeyValueStore.prototype = {
safeLocalStorage[this.context + opts.key] = opts.value;
},
setObject(opts) {
this.set({ key: opts.key, value: JSON.stringify(opts.value) });
},
get(key) {
if (!safeLocalStorage) { return null; }
return safeLocalStorage[this.context + key];
@ -56,9 +62,8 @@ KeyValueStore.prototype = {
getObject(key) {
if (!safeLocalStorage) { return null; }
try {
return JSON.parse(safeLocalStorage[this.context + key]);
} catch(e) {}
try { return JSON.parse(safeLocalStorage[this.context + key]); }
catch (e) { }
}
};
@ -69,5 +74,4 @@ KeyValueStore.prototype.setItem = function(key, value) {
this.set({ key, value });
};
export default KeyValueStore;

View File

@ -3,7 +3,7 @@ import DiscourseURL from 'discourse/lib/url';
const bindings = {
'!': {postAction: 'showFlags'},
'#': {handler: 'toggleProgress', anonymous: true},
'/': {handler: 'showSearch', anonymous: true},
'/': {handler: 'toggleSearch', anonymous: true},
'=': {handler: 'toggleHamburgerMenu', anonymous: true},
'?': {handler: 'showHelpModal', anonymous: true},
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
@ -142,6 +142,11 @@ export default {
},
showBuiltinSearch() {
if (this.container.lookup('controller:header').get('searchVisible')) {
this.toggleSearch();
return true;
}
this.searchService.set('searchContextEnabled', false);
const currentPath = this.container.lookup('controller:application').get('currentPath'),
@ -157,7 +162,7 @@ export default {
if (showSearch) {
this.searchService.set('searchContextEnabled', true);
this.showSearch();
this.toggleSearch();
return false;
}
@ -176,7 +181,7 @@ export default {
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
},
showSearch() {
toggleSearch() {
this.container.lookup('controller:header').send('toggleSearch');
return false;
},

View File

@ -464,7 +464,12 @@ const PostStream = RestModel.extend({
if (this.get('stream').indexOf(postId) === -1) {
this.get('stream').addObject(postId);
if (loadedAllPosts) { this.appendMore(); }
if (loadedAllPosts) {
this.set('loadingLastPost', true);
this.appendMore().finally(
()=>this.set('loadingLastPost', true)
);
}
}
},

View File

@ -25,51 +25,33 @@ function topicsFrom(result, store) {
const TopicList = RestModel.extend({
canLoadMore: Em.computed.notEmpty("more_topics_url"),
forEachNew: function(topics, callback) {
forEachNew(topics, callback) {
const topicIds = [];
_.each(this.get('topics'),function(topic) {
topicIds[topic.get('id')] = true;
});
_.each(topics,function(topic) {
if(!topicIds[topic.id]) {
_.each(this.get('topics'), topic => topicIds[topic.get('id')] = true);
_.each(topics, topic => {
if (!topicIds[topic.id]) {
callback(topic);
}
});
},
refreshSort: function(order, ascending) {
const self = this;
var params = this.get('params') || {};
params.order = order || params.order;
if (ascending === undefined) {
params.ascending = ascending;
} else {
params.ascending = ascending;
}
refreshSort(order, ascending) {
let params = this.get('params') || {};
if (params.q) {
// search is unique, nothing else allowed with it
params = {q: params.q};
params = { q: params.q };
} else {
params.order = order || params.order;
params.ascending = ascending;
}
this.set('loaded', false);
this.set('params', params);
const store = this.store;
store.findFiltered('topicList', {filter: this.get('filter'), params}).then(function(tl) {
const newTopics = tl.get('topics'),
topics = self.get('topics');
topics.clear();
topics.pushObjects(newTopics);
self.setProperties({ loaded: true, more_topics_url: tl.get('topic_list.more_topics_url') });
});
},
loadMore: function() {
loadMore() {
if (this.get('loadingMore')) { return Ember.RSVP.resolve(); }
const moreUrl = this.get('more_topics_url');
@ -108,19 +90,17 @@ const TopicList = RestModel.extend({
// loads topics with these ids "before" the current topics
loadBefore: function(topic_ids){
loadBefore(topic_ids) {
const topicList = this,
topics = this.get('topics');
topics = this.get('topics');
// refresh dupes
topics.removeObjects(topics.filter(function(topic){
return topic_ids.indexOf(topic.get('id')) >= 0;
}));
const url = Discourse.getURL("/") + this.get('filter') + "?topic_ids=" + topic_ids.join(",");
topics.removeObjects(topics.filter(topic => topic_ids.indexOf(topic.get('id')) >= 0));
const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`;
const store = this.store;
return Discourse.ajax({ url }).then(function(result) {
return Discourse.ajax({ url }).then(result => {
let i = 0;
topicList.forEachNew(topicsFrom(result, store), function(t) {
// highlight the first of the new topics so we can get a visual feedback

View File

@ -17,6 +17,10 @@ const User = RestModel.extend({
hasNotPosted: Em.computed.not("hasPosted"),
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
redirected_to_top: {
reason: null,
},
@computed()
stream() {
return UserStream.create({ user: this });

View File

@ -7,36 +7,55 @@ 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(function(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});
site.get('filters').forEach(filter => {
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: function() {
this._super();
willTransition() {
Discourse.User.currentProp("should_be_redirected_to_top", false);
Discourse.User.currentProp("redirected_to_top.reason", null);
return true;
return this._super();
}
}
});
Discourse.DiscoveryTopCategoryRoute = buildCategoryRoute('top');
Discourse.DiscoveryTopParentCategoryRoute = buildCategoryRoute('top');
Discourse.DiscoveryTopCategoryNoneRoute = buildCategoryRoute('top', {no_subcategories: true});
site.get('periods').forEach(function(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});
site.get('periods').forEach(period => {
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('_', '-') });
});
});
@ -87,6 +85,7 @@ export default function() {
this.route('signup', {path: '/signup'});
this.route('login', {path: '/login'});
this.route('login-preferences');
this.route('forgot-password', {path: '/password-reset'});
this.route('faq', {path: '/faq'});
this.route('tos', {path: '/tos'});

View File

@ -1,9 +1,10 @@
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);
@ -63,7 +64,6 @@ export default (filter, params) => {
setupController(controller, model) {
const topics = this.get('topics'),
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
canCreateTopic = topics.get('can_create_topic'),
canCreateTopicOnCategory = model.get('permission') === Discourse.PermissionType.FULL;
@ -72,19 +72,29 @@ export default (filter, params) => {
cannotCreateTopicOnCategory: !canCreateTopicOnCategory,
canCreateTopic: canCreateTopic
});
this.controllerFor('discovery/topics').setProperties({
var topicOpts = {
model: topics,
category: model,
period: periodId,
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
});
};
const p = model.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', model.get('searchContext'));
this.set('topics', null);
@ -100,6 +110,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

@ -0,0 +1,18 @@
import DiscourseRoute from 'discourse/routes/discourse';
export default function(pageName) {
const route = {
model() {
return Discourse.StaticPage.find(pageName);
},
renderTemplate() {
this.render('static');
},
setupController(controller, model) {
this.controllerFor('static').set('model', model);
}
};
return DiscourseRoute.extend(route);
}

View File

@ -15,7 +15,6 @@ function filterQueryParams(params, defaultParams) {
function findTopicList(store, tracking, filter, filterParams, extras) {
extras = extras || {};
return new Ember.RSVP.Promise(function(resolve) {
const session = Discourse.Session.current();
if (extras.cached) {
@ -72,7 +71,7 @@ export default function(filter, extras) {
// attempt to stop early cause we need this to be called before .sync
ScreenTrack.current().stop();
const findOpts = filterQueryParams(transition.queryParams),
const findOpts = filterQueryParams(data),
findExtras = { cached: this.isPoppedState(transition) };
return findTopicList(this.store, this.topicTrackingState, filter, findOpts, findExtras);
@ -85,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
};
@ -116,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

@ -1,22 +1,13 @@
export default Discourse.Route.extend({
beforeModel: function() {
this.replaceWith(this.controllerFor('application').get('loginRequired') ? 'login' : 'discovery').then(function(e) {
Ember.run.next(function() {
e.send('showForgotPassword');
});
import buildStaticRoute from 'discourse/routes/build-static-route';
const ForgotPasswordRoute = buildStaticRoute('password-reset');
ForgotPasswordRoute.reopen({
beforeModel() {
this.replaceWith(this.controllerFor('application').get('loginRequired') ? 'login' : 'discovery').then(e => {
Ember.run.next(() => e.send('showForgotPassword'));
});
},
model: function() {
return Discourse.StaticPage.find('password-reset');
},
renderTemplate: function() {
// do nothing
this.render('static');
},
setupController: function(controller, model) {
this.controllerFor('static').set('model', model);
}
});
export default ForgotPasswordRoute;

View File

@ -1,24 +1,15 @@
export default Discourse.Route.extend({
beforeModel: function() {
if (!Discourse.SiteSettings.login_required) {
this.replaceWith('discovery.latest').then(function(e) {
Ember.run.next(function() {
e.send('showLogin');
});
import buildStaticRoute from 'discourse/routes/build-static-route';
const LoginRoute = buildStaticRoute('login');
LoginRoute.reopen({
beforeModel() {
if (!this.siteSettings.login_required) {
this.replaceWith('discovery.latest').then(e => {
Ember.run.next(() => e.send('showLogin'));
});
}
},
model: function() {
return Discourse.StaticPage.find('login');
},
renderTemplate: function() {
// do nothing
this.render('static');
},
setupController: function(controller, model) {
this.controllerFor('static').set('model', model);
}
});
export default LoginRoute;

View File

@ -2,5 +2,5 @@
<a href="{{unbound category.unreadUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=category.unreadTopics}}'>{{i18n 'filters.unread.lower_title_with_count' count=category.unreadTopics}}</a>
{{/if}}
{{#if category.newTopics}}
<a href="{{unbound category.newUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=ctegory.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=category.newTopics}}</a>
<a href="{{unbound category.newUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=category.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=category.newTopics}}</a>
{{/if}}

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

@ -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

@ -81,6 +81,7 @@
<div class='textarea-wrapper'>
<div class='wmd-button-bar'></div>
<div class='wmd-preview-scroller'></div>
{{conditional-loading-spinner condition=model.loading}}
{{composer-text-area tabindex="4" value=model.reply}}
{{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
</div>
@ -113,7 +114,6 @@
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
</div>
{{/if}}
</div>
{{else}}
<div class='row'>
@ -122,7 +122,7 @@
{{#if model.createdPost}}
{{i18n 'composer.saved'}} <a class='permalink' href="{{unbound createdPost.url}}" {{action "viewNewReply"}}>{{i18n 'composer.view_new_post'}}</a>
{{else}}
{{i18n 'composer.saving'}}
{{i18n 'composer.saving'}} {{loading-spinner size="small"}}
{{/if}}
</div>
<div class='draft-text'>

View File

@ -0,0 +1,8 @@
<div class='container'>
<h2>{{i18n "login.to_continue"}}</h2>
<p style='margin-top: 1em'>{{i18n "login.preferences"}}</p>
{{d-button class="btn-primary" action="showLogin" label="log_in"}}
{{d-button action="showForgotPassword" label="login.forgot"}}
</div>

View File

@ -110,6 +110,7 @@
togglePostType="togglePostType"
rebakePost="rebakePost"
unhidePost="unhidePost"
changePostOwner="changePostOwner"
toggleWhoLiked="toggleWhoLiked"
toggleWhoLikedTarget=view}}
</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

@ -137,7 +137,7 @@
<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>

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

@ -6,32 +6,31 @@ export default Ember.View.extend(CleansUp, {
visible: Em.computed.notEmpty('controller.model'),
_positionChanged: function() {
var pos = this.get('controller.position');
const pos = this.get('controller.position');
if (!pos) { return; }
var $self = this.$();
const $self = this.$();
// Move after we render so the height is correct
Em.run.schedule('afterRender', function() {
var width = $self.width(),
const width = $self.width(),
height = $self.height();
pos.left = (parseInt(pos.left) - (width / 2));
pos.top = (parseInt(pos.top) - (height / 2));
var windowWidth = $(window).width();
const windowWidth = $(window).width();
if (pos.left + width > windowWidth) {
pos.left = (windowWidth - width) - 5;
pos.left = (windowWidth - width) - 15;
}
$self.css(pos);
});
var self = this;
$('html').off('mousedown.topic-entrance').on('mousedown.topic-entrance', function(e) {
var $target = $(e.target);
$('html').off('mousedown.topic-entrance').on('mousedown.topic-entrance', e => {
const $target = $(e.target);
if (($target.prop('id') === 'topic-entrance') || ($self.has($target).length !== 0)) {
return;
}
self.cleanUp();
this.cleanUp();
});
}.observes('controller.position'),
@ -39,12 +38,12 @@ export default Ember.View.extend(CleansUp, {
$('html').off('mousedown.topic-entrance');
}.on('willDestroyElement'),
cleanUp: function() {
cleanUp() {
this.set('controller.model', null);
$('html').off('mousedown.topic-entrance');
},
keyDown: function(e) {
keyDown(e) {
if (e.which === 27) {
this.cleanUp();
}

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

@ -1165,10 +1165,6 @@ table.api-keys {
margin-top: 10px;
}
.pagedown-editor {
width: 98%;
}
textarea.plain {
width: 98%;
height: 200px;
@ -1478,9 +1474,6 @@ and (max-width : 500px) {
.content-editor {
width: 100%;
.pagedown-editor {
box-sizing: border-box;
}
}
div.ac-wrap {

View File

@ -38,6 +38,17 @@
}
}
.textarea-wrapper .spinner {
z-index: 1000;
margin-top: 5em;
}
.saving-text .spinner {
display: inline-block;
left: 5px;
top: 4px;
}
div.ac-wrap.disabled {
input {
display:none;
@ -47,7 +58,6 @@ div.ac-wrap.disabled {
}
}
div.ac-wrap {
background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);

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

@ -9,18 +9,18 @@ body img.emoji {
}
.emoji-modal {
@include transform(translate(-50%, -50%));
z-index: 10000;
position: fixed;
margin-left: -195px;
margin-top: -100px;
left: 50%;
top: 50%;
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

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

View File

@ -237,8 +237,6 @@ blockquote > *:last-child {
background-repeat: repeat-x;
padding: 20px 0;
margin-bottom: 20px;
margin-left: -10px;
margin-right: -10px;
}
}

View File

@ -61,6 +61,11 @@
line-height: 0.8;
}
#suggested-topics .badge-wrapper.bullet span.badge-category,
#suggested-topics .badge-wrapper.bar span.badge-category {
max-width: 150px;
}
.topic-unsubscribe {
.notification-options {
display: inline-block;

View File

@ -32,6 +32,8 @@
vertical-align: text-top;
margin-top: -3px; //vertical alignment fix
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
.extra-info-wrapper & {
color: $header-primary !important;

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

@ -13,8 +13,8 @@ $user_card_background: #222;
box-shadow: 0 2px 12px rgba($primary, .6);
margin-top: -2px;
color: $user_card_primary;
background-size: cover;
background: $user_card_background center center;
background-size: cover;
min-height: 175px;
-webkit-transition: opacity .2s, -webkit-transform .2s;
transition: opacity .2s, transform .2s;

View File

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

View File

@ -20,6 +20,7 @@
@import "mobile/directory";
@import "mobile/menu-panel";
@import "mobile/search";
@import "mobile/emoji";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -0,0 +1,3 @@
.emoji-table-wrapper {
min-width: 320px;
}

View File

@ -523,3 +523,9 @@ span.highlighted {
.small-action.time-gap .topic-avatar {
margin-top: -5px;
}
.gap.jagged-border {
margin-left: -10px;
margin-right: -10px;
}

View File

@ -67,10 +67,6 @@
display: none;
}
.pagedown-editor {
width: 100%;
}
textarea {width: 100%;}
}

View File

@ -4,6 +4,19 @@ class Admin::DiagnosticsController < Admin::AdminController
layout false
skip_before_filter :check_xhr
def dump_statement_cache
statements = Post.exec_sql("select * from pg_prepared_statements").to_a
text = ""
statements.each do |row|
text << "name: #{row["name"]} sql: #{row["statement"]}\n"
end
text << "\n\nCOUNT #{statements.count}"
render text: text, content_type: Mime::TEXT
end
def memory_stats
text = nil

View File

@ -38,6 +38,12 @@ class Admin::EmailController < Admin::AdminController
render json: MultiJson.dump(html_content: renderer.html, text_content: renderer.text)
end
def handle_mail
params.require(:email)
Email::Receiver.new(params[:email]).process
render text: "email was processed"
end
private
def filter_email_logs(email_logs, params)

View File

@ -9,6 +9,10 @@ class Admin::EmbeddingController < Admin::AdminController
end
def update
if params[:embedding][:embed_by_username].blank?
return render_json_error(I18n.t('site_settings.embed_username_required'))
end
Embedding.settings.each do |s|
@embedding.send("#{s}=", params[:embedding][s])
end

View File

@ -295,7 +295,7 @@ class ListController < ApplicationController
return period if top_topics.count >= SiteSetting.topics_per_period_in_top_page
end
# default period is yearly
:yearly
SiteSetting.top_page_default_timeframe
end
def self.best_periods_for(date)

View File

@ -11,7 +11,7 @@ class PermalinksController < ApplicationController
if permalink.external_url
redirect_to permalink.external_url, status: :moved_permanently
elsif permalink.target_url
redirect_to "#{Discourse::base_uri}#{permalink.target_url}", status: :moved_permanently
redirect_to permalink.target_url, status: :moved_permanently
else
raise Discourse::NotFound
end

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)
@ -42,7 +47,7 @@ class PostsController < ApplicationController
.limit(50)
# Remove posts the user doesn't have permission to see
# This isn't leaking any information we weren't already through the post ID numbers
posts = posts.reject { |post| !guardian.can_see?(post) }
posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? }
counts = PostAction.counts_for(posts, current_user)
respond_to do |format|
@ -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

@ -1,14 +1,9 @@
class RobotsTxtController < ApplicationController
layout false
skip_before_filter :preload_json, :check_xhr
skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required
def index
path = if SiteSetting.allow_index_in_robots_txt
:index
else
:no_index
end
path = SiteSetting.allow_index_in_robots_txt ? :index : :no_index
render path, content_type: 'text/plain'
end
end

View File

@ -33,18 +33,37 @@ class Users::OmniauthCallbacksController < ApplicationController
auth[:session] = session
authenticator = self.class.find_authenticator(params[:provider])
provider = Discourse.auth_providers && Discourse.auth_providers.find{|p| p.name == params[:provider]}
@auth_result = authenticator.after_authenticate(auth)
origin = request.env['omniauth.origin']
if origin.present?
parsed = URI.parse(@origin) rescue nil
if parsed
@origin = parsed.path
end
end
unless @origin.present?
@origin = Discourse.base_uri("/")
end
if @auth_result.failed?
flash[:error] = @auth_result.failed_reason.html_safe
return render('failure')
else
@auth_result.authenticator_name = authenticator.name
complete_response_data
respond_to do |format|
format.html
format.json { render json: @auth_result.to_client_hash }
if provider && provider.full_screen_login
flash[:authentication_data] = @auth_result.to_client_hash.to_json
redirect_to @origin
else
respond_to do |format|
format.html
format.json { render json: @auth_result.to_client_hash }
end
end
end
end

View File

@ -162,11 +162,15 @@ class UsersController < ApplicationController
end
def my_redirect
if current_user.present? && params[:path] =~ /^[a-z\-\/]+$/
redirect_to path("/users/#{current_user.username}/#{params[:path]}")
return
raise Discourse::NotFound if params[:path] !~ /^[a-z\-\/]+$/
if current_user.blank?
cookies[:destination_url] = "/my/#{params[:path]}"
redirect_to "/login-preferences"
else
redirect_to(path("/users/#{current_user.username}/#{params[:path]}"))
end
raise Discourse::NotFound
end
def invited

View File

@ -74,6 +74,10 @@ module ApplicationHelper
end
end
def unescape_emoji(title)
PrettyText.unescape_emoji(title)
end
def with_format(format, &block)
old_formats = formats
self.formats = [format]
@ -118,9 +122,7 @@ module ApplicationHelper
# Creates open graph and twitter card meta data
def crawlable_meta_data(opts=nil)
opts ||= {}
opts[:image] ||= "#{Discourse.base_url}#{SiteSetting.logo_small_url}"
opts[:url] ||= "#{Discourse.base_url_no_prefix}#{request.fullpath}"
# Use the correct scheme for open graph
@ -130,21 +132,19 @@ module ApplicationHelper
end
# Add opengraph tags
result = tag(:meta, property: 'og:site_name', content: SiteSetting.title) << "\n"
result = []
result << tag(:meta, property: 'og:site_name', content: SiteSetting.title)
result << tag(:meta, name: 'twitter:card', content: "summary")
# I removed image related opengraph tags from here for now due to
# https://meta.discourse.org/t/x/22744/18
[:url, :title, :description].each do |property|
[:url, :title, :description, :image].each do |property|
if opts[property].present?
escape = (property != :image)
result << tag(:meta, {property: "og:#{property}", content: opts[property]}, nil, escape) << "\n"
result << tag(:meta, {name: "twitter:#{property}", content: opts[property]}, nil, escape) << "\n"
result << tag(:meta, { property: "og:#{property}", content: opts[property] }, nil, escape)
result << tag(:meta, { name: "twitter:#{property}", content: opts[property] }, nil, escape)
end
end
result
result.join("\n")
end
# Look up site content for a key. If the key is blank, you can supply a block and that

View File

@ -3,7 +3,7 @@ module Jobs
def execute(args)
# maybe it was removed by the time we are making the post
if post = Post.find_by(id: args[:post_id])
if post = Post.find(args[:post_id])
# maybe the topic was deleted, so skip in that case as well
PostAlerter.post_created(post) if post.topic
end

View File

@ -205,7 +205,7 @@ SQL
JOIN users u2 ON u2.id = i.user_id
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked
GROUP BY invited_by_id
HAVING COUNT(*) > #{count.to_i}
HAVING COUNT(*) >= #{count.to_i}
) AND u.active AND NOT u.blocked AND u.id > 0 AND
(:backfill OR u.id IN (:user_ids) )
"

View File

@ -75,7 +75,7 @@ class Permalink < ActiveRecord::Base
def target_url
return external_url if external_url
return post.url if post
return "#{Discourse::base_uri}#{post.url}" if post
return topic.relative_url if topic
return category.url if category
nil

View File

@ -106,6 +106,8 @@ class Post < ActiveRecord::Base
id: id,
post_number: post_number,
updated_at: Time.now,
user_id: user_id,
last_editor_id: last_editor_id,
type: type
}

View File

@ -126,9 +126,9 @@ SQL
def self.count_per_day_for_type(post_action_type, opts=nil)
opts ||= {}
opts[:since_days_ago] ||= 30
result = unscoped.where(post_action_type_id: post_action_type)
result = result.where('post_actions.created_at >= ?', opts[:since_days_ago].days.ago)
result = result.where('post_actions.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago)
result = result.where('post_actions.created_at <= ?', opts[:end_date]) if opts[:end_date]
result = result.joins(post: :topic).where('topics.category_id = ?', opts[:category_id]) if opts[:category_id]
result.group('date(post_actions.created_at)')
.order('date(post_actions.created_at)')

View File

@ -35,6 +35,7 @@ class Report
def self.find(type, opts=nil)
opts ||= {}
# Load the report
report = Report.new(type)
report.start_date = opts[:start_date] if opts[:start_date]
@ -184,7 +185,7 @@ class Report
def self.post_action_report(report, post_action_type)
report.data = []
PostAction.count_per_day_for_type(post_action_type, category_id: report.category_id).each do |date, count|
PostAction.count_per_day_for_type(post_action_type, category_id: report.category_id, start_date: report.start_date, end_date: report.end_date).each do |date, count|
report.data << { x: date, y: count }
end
countable = PostAction.unscoped.where(post_action_type_id: post_action_type)

View File

@ -517,18 +517,16 @@ class Topic < ActiveRecord::Base
def add_moderator_post(user, text, opts=nil)
opts ||= {}
new_post = nil
Topic.transaction do
creator = PostCreator.new(user,
raw: text,
post_type: opts[:post_type] || Post.types[:moderator_action],
action_code: opts[:action_code],
no_bump: opts[:bump].blank?,
skip_notifications: opts[:skip_notifications],
topic_id: self.id,
skip_validations: true)
new_post = creator.create
increment!(:moderator_posts_count)
end
creator = PostCreator.new(user,
raw: text,
post_type: opts[:post_type] || Post.types[:moderator_action],
action_code: opts[:action_code],
no_bump: opts[:bump].blank?,
skip_notifications: opts[:skip_notifications],
topic_id: self.id,
skip_validations: true)
new_post = creator.create
increment!(:moderator_posts_count) if new_post.persisted?
if new_post.present?
# If we are moving posts, we want to insert the moderator post where the previous posts were

View File

@ -656,7 +656,9 @@ class User < ActiveRecord::Base
# Flag all posts from a user as spam
def flag_linked_posts_as_spam
admin = Discourse.system_user
topic_links.includes(:post).each do |tl|
disagreed_flag_post_ids = PostAction.where(post_action_type_id: PostActionType.types[:spam]).where.not(disagreed_at: nil).pluck(:post_id)
topic_links.includes(:post).where.not(post_id: disagreed_flag_post_ids).each do |tl|
begin
PostAction.act(admin, tl.post, PostActionType.types[:spam], message: I18n.t('flag_reason.spam_hosts'))
rescue PostAction::AlreadyActed

View File

@ -1,5 +1,6 @@
class PostActionUserSerializer < BasicUserSerializer
attributes :post_url
attributes :post_url,
:username_lower
def id
object.user.id
@ -9,6 +10,10 @@ class PostActionUserSerializer < BasicUserSerializer
object.user.username
end
def username_lower
object.user.username_lower
end
def avatar_template
object.user.avatar_template
end

View File

@ -28,13 +28,13 @@ class SiteSerializer < ApplicationSerializer
end
def post_action_types
cache_fragment("post_action_types") do
cache_fragment("post_action_types_#{I18n.locale}") do
ActiveModel::ArraySerializer.new(PostActionType.ordered).as_json
end
end
def topic_flag_types
cache_fragment("post_action_flag_types") do
cache_fragment("post_action_flag_types_#{I18n.locale}") do
flags = PostActionType.ordered.where(name_key: ['inappropriate', 'spam', 'notify_moderators'])
ActiveModel::ArraySerializer.new(flags, each_serializer: TopicFlagTypeSerializer).as_json
end

View File

@ -53,6 +53,11 @@
<%- end %>
Discourse.S3BaseUrl = '<%= Discourse.store.absolute_base_url %>';
<%- end %>
<%- if !current_user && flash[:authentication_data] %>
Em.run.next(function(){
Discourse.authenticationComplete(<%=flash[:authentication_data].html_safe%>);
});
<%- end %>
</script>
<%= script 'browser-update' %>

View File

@ -49,8 +49,7 @@
<% if @category %>
<% content_for :head do %>
<%= auto_discovery_link_tag(:rss, { action: :category_feed }, title: t('rss_topics_in_category', category: @category.name)) %>
<%= crawlable_meta_data(title: @category.name,
description: @category.description) %>
<%= raw crawlable_meta_data(title: @category.name, description: @category.description) %>
<% end %>
<% end %>

View File

@ -7,7 +7,7 @@
<link><%= @link %></link>
<description><%= @description %></description>
<% @posts.each do |post| %>
<% next unless post.user && post.topic %>
<% next unless post.user %>
<item>
<title><%= post.topic.title %></title>
<dc:creator><![CDATA[<%= "@#{post.user.username}#{" #{post.user.name}" if (post.user.name.present? && SiteSetting.enable_names?)}" -%>]]></dc:creator>

View File

@ -3,9 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><%= @topic_view.topic.title %></title>
<%= crawlable_meta_data(title: @topic_view.title,
description: @topic_view.summary,
image: @topic_view.image_url) %>
<%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary, image: @topic_view.image_url) %>
</head>
<body>
<% @topic_view.posts.each do |post| %>

View File

@ -56,9 +56,7 @@
<% content_for :head do %>
<%= auto_discovery_link_tag(@topic_view, {action: :feed, slug: @topic_view.topic.slug, topic_id: @topic_view.topic.id}, title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %>
<%= crawlable_meta_data(title: @topic_view.title,
description: @topic_view.summary,
image: @topic_view.image_url) %>
<%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary, image: @topic_view.image_url) %>
<% end %>
<% content_for(:title) { "#{@topic_view.page_title}" } %>

View File

@ -20,7 +20,7 @@
<%- @featured_topics.each_with_index do |t, i| %>
<div class='featured-topic'>
<%= link_to t.title, "#{Discourse.base_url}#{t.relative_url}" %>
<a href='<%= Discourse.base_url + t.relative_url %>'><%= raw unescape_emoji(t.title) %></a>
<br/>
<%= category_badge(t.category, inline_style: true, absolute_url: true) %>
</div>
@ -43,7 +43,7 @@
<%- @new_topics.each do |t| %>
<ul>
<li>
<%= link_to t.title, "#{Discourse.base_url}#{t.relative_url}" %>
<a href='<%= Discourse.base_url + t.relative_url %>'><%= raw unescape_emoji(t.title) %></a>
<span class='post-count'><%= t.posts_count %></span>
<%= category_badge(t.category, inline_style: true, absolute_url: true) %>
</li>

View File

@ -22,8 +22,8 @@
<p><%=t "login.close_window" %></p>
<script type="text/javascript">
window.opener.Discourse.authenticationComplete(<%=@auth_result.to_client_hash.to_json.html_safe%>);
window.close();
window.opener.Discourse.authenticationComplete(<%=@auth_result.to_client_hash.to_json.html_safe%>);
window.close();
</script>
</div>
</body>

View File

@ -6,9 +6,9 @@
<% content_for :head do %>
<% if @restrict_fields %>
<%= crawlable_meta_data(title: @user.username, image: @user.small_avatar_url) %>
<%= raw crawlable_meta_data(title: @user.username, image: @user.small_avatar_url) %>
<% else %>
<%= crawlable_meta_data(title: @user.username, description: @user.user_profile.bio_summary, image: @user.small_avatar_url) %>
<%= raw crawlable_meta_data(title: @user.username, description: @user.user_profile.bio_summary, image: @user.small_avatar_url) %>
<% end %>
<% end %>

View File

@ -1,4 +1,5 @@
development:
prepared_statements: false
adapter: postgresql
database: discourse_development
min_messages: warning
@ -25,6 +26,7 @@ test:
# profile db is used for benchmarking using the script/bench.rb script
profile:
prepared_statements: false
adapter: postgresql
database: discourse_profile
min_messages: warning

View File

@ -39,9 +39,9 @@ db_username = discourse
# password used to access the db
db_password =
# allow usage of prepared statements, must be disabled for
# pgpool transaction pooling
db_prepared_statements = true
# Disallow prepared statements
# see: https://github.com/rails/rails/issues/21992
db_prepared_statements = false
# hostname running the forum
hostname = "www.example.com"
@ -123,6 +123,8 @@ new_version_emails = true
connection_reaper_age = 30
# run reap check every 30 seconds
connection_reaper_interval = 30
# also reap any connections older than this
connection_reaper_max_age = 600
# set to relative URL (for subdirectory hosting)
# IMPORTANT: path must not include a trailing /

View File

@ -22,10 +22,10 @@ ar:
few: بايت
many: بايت
other: بايت
gb: جيجا
kb: كيلو بايت
mb: ميجا
tb: تيرا
gb: جيجا بايت
kb: كيلوا بايت
mb: ميجا بايت
tb: تيرا بايت
short:
thousands: "ألف{{number}}"
millions: "مليون{{number}}"
@ -77,8 +77,8 @@ ar:
one: "1 س"
two: "2 س"
few: "%{count} س"
many: "%{count} س"
other: "%{count} س"
many: "%{count} ساعة"
other: "%{count} ساعة"
x_days:
zero: "0 ي"
one: "1 ي"
@ -677,7 +677,6 @@ ar:
user: "المستخدمين المدعويين"
sent: "تم الإرسال"
none: "لا توجد دعوات معلقة لعرضها."
truncated: "اظهار اوائل {{count}} المدعويين"
redeemed: "دعوات مستخدمة"
redeemed_tab: "محررة"
redeemed_tab_with_count: "({{count}}) محررة"

View File

@ -8,6 +8,9 @@
bs_BA:
js:
number:
format:
separator: "."
delimiter: ","
human:
storage_units:
format: '%n %u'
@ -20,6 +23,9 @@ bs_BA:
kb: KB
mb: MB
tb: TB
short:
thousands: "{{number}} hiljada"
millions: "{{number}} miliona"
dates:
time: "h:mm a"
long_no_year: "MMM D h:mm a"
@ -436,7 +442,6 @@ bs_BA:
search: "kucaj da potražiš pozivnice..."
title: "Pozivnice"
user: "Pozvan Korisnik"
truncated: "Showing the first {{count}} invites."
redeemed: "Redeemed Invites"
redeemed_at: "Redeemed"
pending: "Pending Invites"
@ -788,6 +793,8 @@ bs_BA:
'2_4': 'Dobijat ćete notifikacije zato što ste ostavili odgovor na ovoj temi.'
'2_2': 'Dobijat ćete notifikacije zato što pratite ovu temu.'
'2': 'Dobijat ćete notifikacije zato što <a href="/users/{{username}}/preferences">pročitao ovu temu</a>.'
'1_2': 'Dobiti ćete notifikaciju kada neko spomene tvoje @name ili odgovori na tvoj post.'
'1': 'Dobiti ćete notifikaciju kada neko spomene tvoje @name ili odgovori na tvoj post.'
'0_7': 'Ignorišete sve notifikacije u ovoj kategoriji.'
'0_2': 'Ignorišete sve notifikacije u ovoj temi.'
'0': 'Ignorišete sve notifikacije u ovoj temi.'
@ -799,6 +806,12 @@ bs_BA:
title: "Praćenje"
tracking:
title: "Praćenje"
regular:
title: "Regularan"
description: "Dobiti ćete notifikaciju kada neko spomene tvoje @name ili odgovori na tvoj post."
regular_pm:
title: "Regularan"
description: "Dobiti ćete notifikaciju kada neko spomene tvoje @name ili odgovori na tvoj post."
muted_pm:
title: "Mutirano"
description: "You will never be notified of anything about this private message."
@ -819,6 +832,9 @@ bs_BA:
invisible: "Make Unlisted"
visible: "Make Listed"
reset_read: "Reset Read Data"
feature:
pin: "Prikači temu"
unpin: "Otkači temu"
reply:
title: 'Odgovori'
help: 'počni sa pisanjem odgovora na ovu temu'
@ -832,6 +848,8 @@ bs_BA:
title: 'Opomena'
help: 'anonimno prijavi ovu temu ili pošalji privatnu notifikaciju'
success_message: 'Uspješno ste opomenuli ovu temu.'
feature_topic:
title: "Istakni ovu temu."
inviting: "Inviting..."
automatically_add_to_groups_optional: "This invite also includes access to these groups: (optional, admin only)"
automatically_add_to_groups_required: "This invite also includes access to these groups: (<b>Required</b>, admin only)"
@ -1044,6 +1062,9 @@ bs_BA:
title: "Motrenje"
tracking:
title: "Praćenje"
regular:
title: "Regularan"
description: "Dobiti ćete notifikaciju kada neko spomene tvoje @name ili odgovori na tvoj post."
muted:
title: "Mutirano"
flagging:

View File

@ -523,7 +523,6 @@ cs:
search: "pište pro hledání v pozvánkách..."
title: "Pozvánky"
user: "Pozvaný uživatel"
truncated: "Showing the first {{count}} invites."
redeemed: "Uplatněné pozvánky"
redeemed_tab: "Uplatněno"
redeemed_at: "Uplatněno"

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