Version bump
This commit is contained in:
commit
548c18dd51
5
Gemfile
5
Gemfile
@ -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
|
||||
|
||||
18
Gemfile.lock
18
Gemfile.lock
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
263
app/assets/javascripts/discourse/components/d-editor.js.es6
Normal file
263
app/assets/javascripts/discourse/components/d-editor.js.es6
Normal 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}:`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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');
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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});
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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'});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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' });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1,2 +1,2 @@
|
||||
<label>{{i18n 'category.topic_template'}}</label>
|
||||
{{pagedown-editor value=category.topic_template}}
|
||||
{{d-editor value=category.topic_template}}
|
||||
|
||||
@ -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>
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
@ -110,6 +110,7 @@
|
||||
togglePostType="togglePostType"
|
||||
rebakePost="rebakePost"
|
||||
unhidePost="unhidePost"
|
||||
changePostOwner="changePostOwner"
|
||||
toggleWhoLiked="toggleWhoLiked"
|
||||
toggleWhoLikedTarget=view}}
|
||||
</div>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
},
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,4 +10,5 @@
|
||||
@import "common/topic-entrance";
|
||||
@import "common/printer-friendly";
|
||||
@import "common/base/*";
|
||||
@import "common/d-editor";
|
||||
@import "vendor/pikaday";
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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%);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -103,10 +103,6 @@
|
||||
|
||||
.modal.edit-category-modal {
|
||||
.modal-body {
|
||||
.pagedown-editor {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 10em;
|
||||
}
|
||||
|
||||
@ -237,8 +237,6 @@ blockquote > *:last-child {
|
||||
background-repeat: repeat-x;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
94
app/assets/stylesheets/common/d-editor.scss
Normal file
94
app/assets/stylesheets/common/d-editor.scss
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -39,14 +39,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pagedown-editor {
|
||||
width: 450px;
|
||||
|
||||
textarea {
|
||||
width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -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. */
|
||||
|
||||
|
||||
3
app/assets/stylesheets/mobile/emoji.scss
Normal file
3
app/assets/stylesheets/mobile/emoji.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.emoji-table-wrapper {
|
||||
min-width: 320px;
|
||||
}
|
||||
@ -523,3 +523,9 @@ span.highlighted {
|
||||
.small-action.time-gap .topic-avatar {
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.gap.jagged-border {
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
|
||||
@ -67,10 +67,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedown-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {width: 100%;}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) )
|
||||
"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' %>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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| %>
|
||||
|
||||
@ -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}" } %>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 /
|
||||
|
||||
@ -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}}) محررة"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
Reference in New Issue
Block a user