Version bump
This commit is contained in:
commit
cf676ea6b2
@ -90,6 +90,7 @@
|
||||
"no-undef": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-with": 2,
|
||||
"no-this-before-super": 2,
|
||||
"semi": 2,
|
||||
"strict": 0,
|
||||
"valid-typeof": 2,
|
||||
|
||||
@ -40,7 +40,7 @@ cache:
|
||||
|
||||
before_install:
|
||||
- gem install bundler
|
||||
- npm i -g eslint babel-eslint
|
||||
- npm i -g eslint@2.2 babel-eslint
|
||||
- eslint app/assets/javascripts
|
||||
- eslint --ext .es6 app/assets/javascripts
|
||||
- eslint --ext .es6 test/javascripts
|
||||
|
||||
5
Gemfile
5
Gemfile
@ -45,12 +45,14 @@ gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
gem 'ember-rails'
|
||||
gem 'ember-source', '1.12.2'
|
||||
gem 'barber'
|
||||
gem 'babel-transpiler'
|
||||
|
||||
gem 'message_bus', '2.0.0.beta.2'
|
||||
gem 'message_bus', '2.0.0.beta.4'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
@ -78,6 +80,7 @@ gem 'omniauth-openid'
|
||||
gem 'openid-redis-store'
|
||||
gem 'omniauth-facebook'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-instagram'
|
||||
|
||||
# forked while https://github.com/intridea/omniauth-github/pull/41 is being upstreamd
|
||||
gem 'omniauth-github-discourse', require: 'omniauth-github'
|
||||
|
||||
75
Gemfile.lock
75
Gemfile.lock
@ -1,38 +1,38 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activejob (= 4.2.5.1)
|
||||
actionmailer (4.2.5.2)
|
||||
actionpack (= 4.2.5.2)
|
||||
actionview (= 4.2.5.2)
|
||||
activejob (= 4.2.5.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
actionpack (4.2.5.2)
|
||||
actionview (= 4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
actionview (4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
active_model_serializers (0.8.3)
|
||||
activemodel (>= 3.0)
|
||||
activejob (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
activejob (4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
activemodel (4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.5.1)
|
||||
activemodel (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
activerecord (4.2.5.2)
|
||||
activemodel (= 4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.5.1)
|
||||
activesupport (4.2.5.2)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
@ -127,6 +127,7 @@ GEM
|
||||
htmlentities (4.3.4)
|
||||
http-cookie (1.0.2)
|
||||
domain_name (~> 0.5)
|
||||
http_accept_language (2.0.5)
|
||||
i18n (0.7.0)
|
||||
image_optim (0.20.2)
|
||||
exifr (~> 1.1, >= 1.1.3)
|
||||
@ -156,12 +157,11 @@ GEM
|
||||
mail (2.6.3)
|
||||
mime-types (>= 1.16, < 3)
|
||||
memory_profiler (0.9.6)
|
||||
message_bus (2.0.0.beta.2)
|
||||
message_bus (2.0.0.beta.4)
|
||||
rack (>= 1.1.3)
|
||||
redis
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99)
|
||||
mime-types (2.99.1)
|
||||
mini_portile2 (2.0.0)
|
||||
minitest (5.8.4)
|
||||
mocha (1.1.0)
|
||||
@ -199,6 +199,9 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.3.1)
|
||||
omniauth-instagram (1.0.2)
|
||||
omniauth (~> 1)
|
||||
omniauth-oauth2 (~> 1)
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
@ -232,8 +235,8 @@ GEM
|
||||
puma (2.15.3)
|
||||
r2 (0.2.6)
|
||||
rack (1.6.4)
|
||||
rack-mini-profiler (0.9.8)
|
||||
rack (>= 1.1.3)
|
||||
rack-mini-profiler (0.9.9.2)
|
||||
rack (>= 1.2.0)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
ruby-openid (>= 2.1.8)
|
||||
@ -241,16 +244,16 @@ GEM
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.2.5.1)
|
||||
actionmailer (= 4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activejob (= 4.2.5.1)
|
||||
activemodel (= 4.2.5.1)
|
||||
activerecord (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
rails (4.2.5.2)
|
||||
actionmailer (= 4.2.5.2)
|
||||
actionpack (= 4.2.5.2)
|
||||
actionview (= 4.2.5.2)
|
||||
activejob (= 4.2.5.2)
|
||||
activemodel (= 4.2.5.2)
|
||||
activerecord (= 4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.5.1)
|
||||
railties (= 4.2.5.2)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
@ -263,9 +266,9 @@ GEM
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
rails_multisite (1.0.3)
|
||||
railties (4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
railties (4.2.5.2)
|
||||
actionpack (= 4.2.5.2)
|
||||
activesupport (= 4.2.5.2)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
raindrops (0.15.0)
|
||||
@ -427,6 +430,7 @@ DEPENDENCIES
|
||||
highline
|
||||
hiredis
|
||||
htmlentities
|
||||
http_accept_language (~> 2.0.5)
|
||||
image_optim (= 0.20.2)
|
||||
librarian (>= 0.0.25)
|
||||
listen (= 0.7.3)
|
||||
@ -434,7 +438,7 @@ DEPENDENCIES
|
||||
lru_redux
|
||||
mail
|
||||
memory_profiler
|
||||
message_bus (= 2.0.0.beta.2)
|
||||
message_bus (= 2.0.0.beta.4)
|
||||
mime-types
|
||||
minitest
|
||||
mocha
|
||||
@ -447,6 +451,7 @@ DEPENDENCIES
|
||||
omniauth-facebook
|
||||
omniauth-github-discourse
|
||||
omniauth-google-oauth2
|
||||
omniauth-instagram
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
|
||||
@ -328,7 +328,7 @@ const AdminUser = Discourse.User.extend({
|
||||
}).then(function(data) {
|
||||
if (data.success) {
|
||||
if (data.username) {
|
||||
document.location = Discourse.getURL("/admin/users/" + data.username);
|
||||
document.location = Discourse.getURL("/admin/users/" + user.get('id') + "/" + data.username);
|
||||
} else {
|
||||
document.location = Discourse.getURL("/admin/users/list/active");
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export default Discourse.Route.extend({
|
||||
|
||||
actions: {
|
||||
exportUsers() {
|
||||
exportEntity('user_list').then(outputExportResult);
|
||||
exportEntity('user_list', {trust_level: this.controllerFor('admin-users-list-show').get('query')}).then(outputExportResult);
|
||||
},
|
||||
|
||||
sendInvites() {
|
||||
|
||||
@ -285,7 +285,7 @@
|
||||
{{#each r in top_referrers.data}}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">{{#link-to 'adminUser' r}}{{unbound r.username}}{{/link-to}}</td>
|
||||
<td class="title">{{#link-to 'adminUser' r.user_id r.username}}{{unbound r.username}}{{/link-to}}</td>
|
||||
<td class="value">{{r.num_clicks}}</td>
|
||||
<td class="value">{{r.num_topics}}</td>
|
||||
</tr>
|
||||
|
||||
@ -152,21 +152,20 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
})
|
||||
});
|
||||
|
||||
function proxyDep(propName, moduleFunc, msg) {
|
||||
if (Discourse.hasOwnProperty(propName)) { return; }
|
||||
Object.defineProperty(Discourse, propName, {
|
||||
get: function() {
|
||||
msg = msg || "import the module";
|
||||
Ember.warn("DEPRECATION: `Discourse." + propName + "` is deprecated, " + msg + ".");
|
||||
return moduleFunc();
|
||||
}
|
||||
});
|
||||
function RemovedObject(name) {
|
||||
this._removedName = name;
|
||||
}
|
||||
|
||||
proxyDep('computed', function() { return require('discourse/lib/computed'); });
|
||||
proxyDep('Formatter', function() { return require('discourse/lib/formatter'); });
|
||||
proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default; });
|
||||
proxyDep('URL', function() { return require('discourse/lib/url').default; });
|
||||
proxyDep('Quote', function() { return require('discourse/lib/quote').default; });
|
||||
proxyDep('debounce', function() { return require('discourse/lib/debounce').default; });
|
||||
proxyDep('View', function() { return Ember.View; }, "Use `Ember.View` instead");
|
||||
function methodMissing() {
|
||||
console.warn("The " + this._removedName + " object has been removed from Discourse " +
|
||||
"and your plugin needs to be updated.");
|
||||
};
|
||||
|
||||
Discourse.RemovedObject = RemovedObject;
|
||||
|
||||
['reopen', 'registerButton', 'on', 'off'].forEach(function(m) { RemovedObject.prototype[m] = methodMissing; });
|
||||
|
||||
['discourse/views/post', 'discourse/components/post-menu'].forEach(function(moduleName) {
|
||||
define(moduleName, [], function() { return new RemovedObject(moduleName); });
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
find(store, type, findArgs) {
|
||||
const maxReplies = Discourse.SiteSettings.max_reply_history;
|
||||
return Discourse.ajax(`/posts/${findArgs.postId}/reply-history?max_replies=${maxReplies}`).then(replies => {
|
||||
return { post_reply_histories: replies };
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
find(store, type, findArgs) {
|
||||
return Discourse.ajax(`/posts/${findArgs.postId}/replies`).then(replies => {
|
||||
return { post_replies: replies };
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -1,122 +0,0 @@
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend(StringBuffer, {
|
||||
tagName: 'section',
|
||||
classNameBindings: [':post-actions', 'hidden'],
|
||||
actionsSummary: Em.computed.alias('post.actionsWithoutLikes'),
|
||||
emptySummary: Em.computed.empty('actionsSummary'),
|
||||
hidden: Em.computed.and('emptySummary', 'post.notDeleted'),
|
||||
usersByType: null,
|
||||
|
||||
rerenderTriggers: ['actionsSummary.@each', 'post.deleted'],
|
||||
|
||||
@on('init')
|
||||
initUsersByType() {
|
||||
this.set('usersByType', {});
|
||||
},
|
||||
|
||||
// This was creating way too many bound ifs and subviews in the handlebars version.
|
||||
renderString(buffer) {
|
||||
const usersByType = this.get('usersByType');
|
||||
|
||||
if (!this.get('emptySummary')) {
|
||||
this.get('actionsSummary').forEach(function(c) {
|
||||
const id = c.get('id');
|
||||
const users = usersByType[id] || [];
|
||||
|
||||
buffer.push("<div class='post-action'>");
|
||||
|
||||
const renderLink = (dataAttribute, text) => {
|
||||
buffer.push(` <span class='action-link ${dataAttribute}-action'><a href data-${dataAttribute}='${id}'>${text}</a>.</span>`);
|
||||
};
|
||||
|
||||
// TODO multi line expansion for flags
|
||||
let iconsHtml = "";
|
||||
if (users.length) {
|
||||
let postUrl;
|
||||
users.forEach(function(u) {
|
||||
const username = u.get('username');
|
||||
|
||||
iconsHtml += `<a href="${Discourse.getURL("/users")}${username}" data-user-card="${username}">`;
|
||||
if (u.post_url) {
|
||||
postUrl = postUrl || u.post_url;
|
||||
}
|
||||
iconsHtml += Discourse.Utilities.avatarImg({
|
||||
size: 'small',
|
||||
avatarTemplate: u.get('avatar_template'),
|
||||
title: u.get('username')
|
||||
});
|
||||
iconsHtml += "</a>";
|
||||
});
|
||||
|
||||
let key = 'post.actions.people.' + c.get('actionType.name_key');
|
||||
if (postUrl) { key = key + "_with_url"; }
|
||||
|
||||
// TODO postUrl might be uninitialized? pick a good default
|
||||
buffer.push(" " + I18n.t(key, { icons: iconsHtml, postUrl }) + ".");
|
||||
}
|
||||
|
||||
if (users.length === 0) {
|
||||
renderLink('who-acted', c.get('description'));
|
||||
}
|
||||
|
||||
if (c.get('can_undo')) {
|
||||
renderLink('undo', I18n.t("post.actions.undo." + c.get('actionType.name_key')));
|
||||
}
|
||||
if (c.get('can_defer_flags')) {
|
||||
renderLink('defer-flags', I18n.t("post.actions.defer_flags", { count: c.count }));
|
||||
}
|
||||
|
||||
|
||||
buffer.push("</div>");
|
||||
});
|
||||
}
|
||||
|
||||
const post = this.get('post');
|
||||
if (post.get('deleted')) {
|
||||
buffer.push("<div class='post-action'>" +
|
||||
iconHTML('fa-trash-o') + ' ' +
|
||||
Discourse.Utilities.tinyAvatar(post.get('postDeletedBy.avatar_template'), {title: post.get('postDeletedBy.username')}) +
|
||||
autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) +
|
||||
"</div>");
|
||||
}
|
||||
|
||||
buffer.push("<div class='clearfix'></div>");
|
||||
},
|
||||
|
||||
actionTypeById(actionTypeId) {
|
||||
return this.get('actionsSummary').findProperty('id', actionTypeId);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
const $target = $(e.target);
|
||||
let actionTypeId;
|
||||
|
||||
const post = this.get('post');
|
||||
|
||||
if (actionTypeId = $target.data('defer-flags')) {
|
||||
this.actionTypeById(actionTypeId).deferFlags(post);
|
||||
return false;
|
||||
}
|
||||
|
||||
// User wants to know who actioned it
|
||||
const usersByType = this.get('usersByType');
|
||||
if (actionTypeId = $target.data('who-acted')) {
|
||||
this.actionTypeById(actionTypeId).loadUsers(post).then(users => {
|
||||
usersByType[actionTypeId] = users;
|
||||
this.rerender();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actionTypeId = $target.data('undo')) {
|
||||
this.get('actionsSummary').findProperty('id', actionTypeId).undo(post);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -12,8 +12,8 @@ export default Ember.Component.extend({
|
||||
return !c.get('parentCategory');
|
||||
}),
|
||||
|
||||
hidden: function(){
|
||||
return Discourse.Mobile.mobileView && !this.get('category');
|
||||
hidden: function() {
|
||||
return this.site.mobileView && !this.get('category');
|
||||
}.property('category'),
|
||||
|
||||
firstCategory: function() {
|
||||
@ -35,7 +35,7 @@ export default Ember.Component.extend({
|
||||
});
|
||||
}.property('firstCategory', 'hideSubcategories'),
|
||||
|
||||
render: function(buffer) {
|
||||
render(buffer) {
|
||||
if (this.get('hidden')) { return; }
|
||||
this._super(buffer);
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@on('init')
|
||||
_setupPreview() {
|
||||
const val = (Discourse.Mobile.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
||||
const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
||||
this.set('showPreview', val === 'true');
|
||||
},
|
||||
|
||||
@ -91,6 +91,8 @@ export default Ember.Component.extend({
|
||||
|
||||
_syncEditorAndPreviewScroll() {
|
||||
const $input = this.$('.d-editor-input');
|
||||
if (!$input) { return; }
|
||||
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
|
||||
if ($input.scrollTop() === 0) {
|
||||
@ -216,7 +218,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
});
|
||||
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
if (this.site.mobileView) {
|
||||
this.$(".mobile-file-upload").on("click.uploader", function () {
|
||||
// redirect the click on the hidden file input
|
||||
$("#mobile-uploader").click();
|
||||
|
||||
@ -25,156 +25,164 @@ const OP = {
|
||||
|
||||
const _createCallbacks = [];
|
||||
|
||||
function Toolbar() {
|
||||
this.shortcuts = {};
|
||||
class Toolbar {
|
||||
|
||||
this.groups = [
|
||||
{group: 'fontStyles', buttons: []},
|
||||
{group: 'insertions', buttons: []},
|
||||
{group: 'extras', buttons: []}
|
||||
];
|
||||
constructor(site) {
|
||||
this.shortcuts = {};
|
||||
|
||||
this.addButton({
|
||||
id: 'bold',
|
||||
group: 'fontStyles',
|
||||
shortcut: 'B',
|
||||
perform: e => e.applySurround('**', '**', 'bold_text')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'italic',
|
||||
group: 'fontStyles',
|
||||
shortcut: 'I',
|
||||
perform: e => e.applySurround('_', '_', 'italic_text')
|
||||
});
|
||||
|
||||
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
|
||||
|
||||
this.addButton({
|
||||
id: 'quote',
|
||||
group: 'insertions',
|
||||
icon: 'quote-right',
|
||||
shortcut: 'Shift+9',
|
||||
perform: e => e.applySurround('> ', '', 'code_text')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'code',
|
||||
group: 'insertions',
|
||||
shortcut: 'Shift+C',
|
||||
perform(e) {
|
||||
if (e.selected.value.indexOf("\n") !== -1) {
|
||||
e.applySurround(' ', '', 'code_text');
|
||||
} else {
|
||||
e.applySurround('`', '`', 'code_text');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'bullet',
|
||||
group: 'extras',
|
||||
icon: 'list-ul',
|
||||
shortcut: 'Shift+8',
|
||||
title: 'composer.ulist_title',
|
||||
perform: e => e.applyList('* ', 'list_item')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'list',
|
||||
group: 'extras',
|
||||
icon: 'list-ol',
|
||||
shortcut: 'Shift+7',
|
||||
title: 'composer.olist_title',
|
||||
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'heading',
|
||||
group: 'extras',
|
||||
icon: 'font',
|
||||
shortcut: 'Alt+1',
|
||||
perform: e => e.applyList('## ', 'heading_text')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'rule',
|
||||
group: 'extras',
|
||||
icon: 'minus',
|
||||
shortcut: 'Alt+R',
|
||||
title: 'composer.hr_title',
|
||||
perform: e => e.addText("\n\n----------\n")
|
||||
});
|
||||
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
this.groups.push({group: 'mobileExtras', buttons: []});
|
||||
this.groups = [
|
||||
{group: 'fontStyles', buttons: []},
|
||||
{group: 'insertions', buttons: []},
|
||||
{group: 'extras', buttons: []}
|
||||
];
|
||||
|
||||
this.addButton({
|
||||
id: 'preview',
|
||||
group: 'mobileExtras',
|
||||
icon: 'television',
|
||||
title: 'composer.hr_preview',
|
||||
perform: e => e.preview()
|
||||
id: 'bold',
|
||||
group: 'fontStyles',
|
||||
shortcut: 'B',
|
||||
perform: e => e.applySurround('**', '**', 'bold_text')
|
||||
});
|
||||
}
|
||||
|
||||
this.groups[this.groups.length-1].lastGroup = true;
|
||||
};
|
||||
this.addButton({
|
||||
id: 'italic',
|
||||
group: 'fontStyles',
|
||||
shortcut: 'I',
|
||||
perform: e => e.applySurround('_', '_', 'italic_text')
|
||||
});
|
||||
|
||||
Toolbar.prototype.addButton = function(button) {
|
||||
const g = this.groups.findProperty('group', button.group);
|
||||
if (!g) {
|
||||
throw `Couldn't find toolbar group ${button.group}`;
|
||||
}
|
||||
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
|
||||
|
||||
const createdButton = {
|
||||
id: button.id,
|
||||
className: button.className || button.id,
|
||||
icon: button.icon || button.id,
|
||||
action: button.action || 'toolbarButton',
|
||||
perform: button.perform || Ember.K
|
||||
};
|
||||
this.addButton({
|
||||
id: 'quote',
|
||||
group: 'insertions',
|
||||
icon: 'quote-right',
|
||||
shortcut: 'Shift+9',
|
||||
perform: e => e.applySurround('> ', '', 'code_text')
|
||||
});
|
||||
|
||||
if (button.sendAction) {
|
||||
createdButton.sendAction = button.sendAction;
|
||||
}
|
||||
this.addButton({
|
||||
id: 'code',
|
||||
group: 'insertions',
|
||||
shortcut: 'Shift+C',
|
||||
perform(e) {
|
||||
if (e.selected.value.indexOf("\n") !== -1) {
|
||||
e.applySurround(' ', '', 'code_text');
|
||||
} else {
|
||||
e.applySurround('`', '`', 'code_text');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const title = I18n.t(button.title || `composer.${button.id}_title`);
|
||||
if (button.shortcut) {
|
||||
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
const mod = mac ? 'Meta' : 'Ctrl';
|
||||
var shortcutTitle = `${mod}+${button.shortcut}`;
|
||||
this.addButton({
|
||||
id: 'bullet',
|
||||
group: 'extras',
|
||||
icon: 'list-ul',
|
||||
shortcut: 'Shift+8',
|
||||
title: 'composer.ulist_title',
|
||||
perform: e => e.applyList('* ', 'list_item')
|
||||
});
|
||||
|
||||
// Mac users are used to glyphs for shortcut keys
|
||||
if (mac) {
|
||||
shortcutTitle = shortcutTitle
|
||||
.replace('Shift', "\u21E7")
|
||||
.replace('Meta', "\u2318")
|
||||
.replace('Alt', "\u2325")
|
||||
.replace(/\+/g, '');
|
||||
} else {
|
||||
shortcutTitle = shortcutTitle
|
||||
.replace('Shift', I18n.t('shortcut_modifier_key.shift'))
|
||||
.replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl'))
|
||||
.replace('Alt', I18n.t('shortcut_modifier_key.alt'));
|
||||
this.addButton({
|
||||
id: 'list',
|
||||
group: 'extras',
|
||||
icon: 'list-ol',
|
||||
shortcut: 'Shift+7',
|
||||
title: 'composer.olist_title',
|
||||
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'heading',
|
||||
group: 'extras',
|
||||
icon: 'font',
|
||||
shortcut: 'Alt+1',
|
||||
perform: e => e.applyList('## ', 'heading_text')
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
id: 'rule',
|
||||
group: 'extras',
|
||||
icon: 'minus',
|
||||
shortcut: 'Alt+R',
|
||||
title: 'composer.hr_title',
|
||||
perform: e => e.addText("\n\n----------\n")
|
||||
});
|
||||
|
||||
if (site.mobileView) {
|
||||
this.groups.push({group: 'mobileExtras', buttons: []});
|
||||
|
||||
this.addButton({
|
||||
id: 'preview',
|
||||
group: 'mobileExtras',
|
||||
icon: 'television',
|
||||
title: 'composer.hr_preview',
|
||||
perform: e => e.preview()
|
||||
});
|
||||
}
|
||||
|
||||
createdButton.title = `${title} (${shortcutTitle})`;
|
||||
|
||||
this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
|
||||
} else {
|
||||
createdButton.title = title;
|
||||
this.groups[this.groups.length-1].lastGroup = true;
|
||||
}
|
||||
|
||||
if (button.unshift) {
|
||||
g.buttons.unshift(createdButton);
|
||||
} else {
|
||||
g.buttons.push(createdButton);
|
||||
addButton(button) {
|
||||
const g = this.groups.findProperty('group', button.group);
|
||||
if (!g) {
|
||||
throw `Couldn't find toolbar group ${button.group}`;
|
||||
}
|
||||
|
||||
const createdButton = {
|
||||
id: button.id,
|
||||
className: button.className || button.id,
|
||||
icon: button.icon || button.id,
|
||||
action: button.action || 'toolbarButton',
|
||||
perform: button.perform || Ember.K
|
||||
};
|
||||
|
||||
if (button.sendAction) {
|
||||
createdButton.sendAction = button.sendAction;
|
||||
}
|
||||
|
||||
const title = I18n.t(button.title || `composer.${button.id}_title`);
|
||||
if (button.shortcut) {
|
||||
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
const mod = mac ? 'Meta' : 'Ctrl';
|
||||
var shortcutTitle = `${mod}+${button.shortcut}`;
|
||||
|
||||
// Mac users are used to glyphs for shortcut keys
|
||||
if (mac) {
|
||||
shortcutTitle = shortcutTitle
|
||||
.replace('Shift', "\u21E7")
|
||||
.replace('Meta', "\u2318")
|
||||
.replace('Alt', "\u2325")
|
||||
.replace(/\+/g, '');
|
||||
} else {
|
||||
shortcutTitle = shortcutTitle
|
||||
.replace('Shift', I18n.t('shortcut_modifier_key.shift'))
|
||||
.replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl'))
|
||||
.replace('Alt', I18n.t('shortcut_modifier_key.alt'));
|
||||
}
|
||||
|
||||
createdButton.title = `${title} (${shortcutTitle})`;
|
||||
|
||||
this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
|
||||
} else {
|
||||
createdButton.title = title;
|
||||
}
|
||||
|
||||
if (button.unshift) {
|
||||
g.buttons.unshift(createdButton);
|
||||
} else {
|
||||
g.buttons.push(createdButton);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function addToolbarCallback(func) {
|
||||
_createCallbacks.push(func);
|
||||
}
|
||||
|
||||
export function onToolbarCreate(func) {
|
||||
_createCallbacks.push(func);
|
||||
console.warn('`onToolbarCreate` is deprecated, use the plugin api instead.');
|
||||
addToolbarCallback(func);
|
||||
};
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@ -237,7 +245,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed
|
||||
toolbar() {
|
||||
const toolbar = new Toolbar();
|
||||
const toolbar = new Toolbar(this.site);
|
||||
_createCallbacks.forEach(cb => cb(toolbar));
|
||||
this.sendAction('extraButtons', toolbar);
|
||||
return toolbar;
|
||||
@ -293,6 +301,9 @@ export default Ember.Component.extend({
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
key: ":",
|
||||
afterComplete(text) {
|
||||
self.set('value', text);
|
||||
},
|
||||
|
||||
transformComplete(v) {
|
||||
if (v.code) {
|
||||
@ -301,7 +312,16 @@ export default Ember.Component.extend({
|
||||
showSelector({
|
||||
appendTo: self.$(),
|
||||
container,
|
||||
onSelect: title => self._addText(self._getSelected(), `${title}:`)
|
||||
onSelect: title => {
|
||||
// Remove the previously type characters when a new emoji is selected from the selector.
|
||||
let selected = self._getSelected();
|
||||
let newPre = selected.pre.replace(/:[^:]+$/, ":");
|
||||
let numOfRemovedChars = selected.pre.length - newPre.length;
|
||||
selected.pre = newPre;
|
||||
selected.start -= numOfRemovedChars;
|
||||
selected.end -= numOfRemovedChars;
|
||||
self._addText(selected, `${title}:`);
|
||||
}
|
||||
});
|
||||
return "";
|
||||
}
|
||||
@ -313,7 +333,7 @@ export default Ember.Component.extend({
|
||||
term = term.toLowerCase();
|
||||
|
||||
if (term === "") {
|
||||
return resolve(["slightly_smiling", "smile", "wink", "sunny", "blush"]);
|
||||
return resolve(["slight_smile", "smile", "wink", "sunny", "blush"]);
|
||||
}
|
||||
|
||||
if (Discourse.Emoji.translations[full]) {
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import LogsNotice from 'discourse/services/logs-notice';
|
||||
|
||||
export default Ember.Component.extend(StringBuffer, {
|
||||
rerenderTriggers: ['site.isReadOnly'],
|
||||
@ -18,8 +21,33 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
notices.push([this.siteSettings.global_notice, 'alert-global-notice']);
|
||||
}
|
||||
|
||||
if (notices.length > 0) {
|
||||
buffer.push(_.map(notices, n => "<div class='row'><div class='alert alert-info " + n[1] + "'>" + n[0] + "</div></div>").join(""));
|
||||
if (!LogsNotice.currentProp('hidden')) {
|
||||
notices.push([LogsNotice.currentProp('message'), 'alert-logs-notice', `<div class='close'>${iconHTML('times')}</div>`]);
|
||||
}
|
||||
|
||||
if (notices.length > 0) {
|
||||
buffer.push(_.map(notices, n => {
|
||||
var html = `<div class='row'><div class='alert alert-info ${n[1]}'>${n[0]}`;
|
||||
if (n[2]) html += n[2];
|
||||
html += '</div></div>';
|
||||
return html;
|
||||
}).join(""));
|
||||
}
|
||||
},
|
||||
|
||||
@on('didInsertElement')
|
||||
_setupLogsNotice() {
|
||||
LogsNotice.current().addObserver('hidden', () => {
|
||||
this.rerenderString();
|
||||
});
|
||||
|
||||
this.$().on('click.global-notice', '.alert-logs-notice .close', () => {
|
||||
LogsNotice.currentProp('text', '');
|
||||
});
|
||||
},
|
||||
|
||||
@on('willDestroyElement')
|
||||
_teardownLogsNotice() {
|
||||
this.$().off('click.global-notice');
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNames: ['category-link'],
|
||||
|
||||
@computed('category.unreadTopics', 'category.newTopics')
|
||||
unreadTotal(unreadTopics, newTopics) {
|
||||
return parseInt(unreadTopics, 10) + parseInt(newTopics, 10);
|
||||
},
|
||||
|
||||
showTopicCount: Ember.computed.not('currentUser')
|
||||
});
|
||||
@ -1,4 +1,6 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import mobile from 'discourse/lib/mobile';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['hamburger-panel'],
|
||||
|
||||
@ -10,17 +12,17 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed()
|
||||
showKeyboardShortcuts() {
|
||||
return !Discourse.Mobile.mobileView && !this.capabilities.touch;
|
||||
return !this.site.mobileView && !this.capabilities.touch;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showMobileToggle() {
|
||||
return Discourse.Mobile.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
},
|
||||
|
||||
@computed()
|
||||
mobileViewLinkTextKey() {
|
||||
return Discourse.Mobile.mobileView ? "desktop_view" : "mobile_view";
|
||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
||||
},
|
||||
|
||||
@computed()
|
||||
@ -68,7 +70,7 @@ export default Ember.Component.extend({
|
||||
this.sendAction('showKeyboardAction');
|
||||
},
|
||||
toggleMobileView() {
|
||||
Discourse.Mobile.toggleMobileView();
|
||||
mobile.toggleMobileView();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -17,7 +17,7 @@ export default Ember.Component.extend({
|
||||
if (this.siteSettings.login_required && !this.currentUser) {
|
||||
this.sendAction('loginAction');
|
||||
} else {
|
||||
if (Discourse.Mobile.mobileView && this.get('mobileAction')) {
|
||||
if (this.site.mobileView && this.get('mobileAction')) {
|
||||
this.sendAction('mobileAction');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -14,11 +14,11 @@ export default Ember.Component.extend({
|
||||
}.property('targetUrl'),
|
||||
|
||||
showSmallLogo: function() {
|
||||
return !Discourse.Mobile.mobileView && this.get("minimized");
|
||||
return !this.site.mobileView && this.get("minimized");
|
||||
}.property("minimized"),
|
||||
|
||||
showMobileLogo: function() {
|
||||
return Discourse.Mobile.mobileView && !Ember.isBlank(this.get('mobileBigLogoUrl'));
|
||||
return this.site.mobileView && !Ember.isBlank(this.get('mobileBigLogoUrl'));
|
||||
}.property(),
|
||||
|
||||
smallLogoUrl: setting('logo_small_url'),
|
||||
|
||||
@ -120,17 +120,17 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed()
|
||||
showKeyboardShortcuts() {
|
||||
return !Discourse.Mobile.mobileView && !this.capabilities.touch;
|
||||
return !this.site.mobileView && !this.capabilities.touch;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showMobileToggle() {
|
||||
return Discourse.Mobile.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
},
|
||||
|
||||
@computed()
|
||||
mobileViewLinkTextKey() {
|
||||
return Discourse.Mobile.mobileView ? "desktop_view" : "mobile_view";
|
||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
||||
},
|
||||
|
||||
@computed()
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
import { diff, patch } from 'virtual-dom';
|
||||
import { WidgetClickHook } from 'discourse/widgets/click-hook';
|
||||
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
||||
|
||||
const _cleanCallbacks = {};
|
||||
export function addWidgetCleanCallback(widgetName, fn) {
|
||||
_cleanCallbacks[widgetName] = _cleanCallbacks[widgetName] || [];
|
||||
_cleanCallbacks[widgetName].push(fn);
|
||||
}
|
||||
|
||||
export default Ember.Component.extend({
|
||||
_tree: null,
|
||||
_rootNode: null,
|
||||
_timeout: null,
|
||||
_widgetClass: null,
|
||||
_afterRender: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
const name = this.get('widget');
|
||||
|
||||
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
||||
this._connected = [];
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
WidgetClickHook.setupDocumentCallback();
|
||||
|
||||
this._rootNode = document.createElement('div');
|
||||
this.element.appendChild(this._rootNode);
|
||||
this._timeout = Ember.run.scheduleOnce('render', this, this.rerenderWidget);
|
||||
},
|
||||
|
||||
willClearRender() {
|
||||
const callbacks = _cleanCallbacks[this.get('widget')];
|
||||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb());
|
||||
}
|
||||
|
||||
this._connected.forEach(v => v.destroy());
|
||||
this._connected.length = 0;
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
Ember.run.cancel(this._timeout);
|
||||
},
|
||||
|
||||
queueRerender(callback) {
|
||||
if (callback && !this._afterRender) {
|
||||
this._afterRender = callback;
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce('render', this, this.rerenderWidget);
|
||||
},
|
||||
|
||||
rerenderWidget() {
|
||||
Ember.run.cancel(this._timeout);
|
||||
if (this._rootNode) {
|
||||
const opts = { model: this.get('model') };
|
||||
const newTree = new this._widgetClass(this.get('args'), this.container, opts);
|
||||
|
||||
newTree._emberView = this;
|
||||
const patches = diff(this._tree || this._rootNode, newTree);
|
||||
|
||||
const $body = $(document);
|
||||
const prevHeight = $body.height();
|
||||
const prevScrollTop = $body.scrollTop();
|
||||
|
||||
this._rootNode = patch(this._rootNode, patches);
|
||||
|
||||
const height = $body.height();
|
||||
const scrollTop = $body.scrollTop();
|
||||
|
||||
// This hack is for when swapping out many cloaked views at once
|
||||
// when using keyboard navigation. It could suddenly move the
|
||||
// scroll
|
||||
if (prevHeight === height && scrollTop !== prevScrollTop) {
|
||||
$body.scrollTop(prevScrollTop);
|
||||
}
|
||||
|
||||
this._tree = newTree;
|
||||
|
||||
if (this._afterRender) {
|
||||
this._afterRender();
|
||||
this._afterRender = null;
|
||||
}
|
||||
|
||||
renderedKey('*');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
const LIKED_TYPE = 5;
|
||||
const INVITED_TYPE = 8;
|
||||
const GROUP_SUMMARY_TYPE = 16;
|
||||
|
||||
@ -80,7 +81,16 @@ export default Ember.Component.extend({
|
||||
const count = notification.get('data.inbox_count');
|
||||
const group_name = notification.get('data.group_name');
|
||||
text = I18n.t(this.get('scope'), {count, group_name});
|
||||
} else {
|
||||
} else if (notification.get('notification_type') === LIKED_TYPE && notification.get("data.count") > 1) {
|
||||
const count = notification.get('data.count') - 2;
|
||||
const username2 = notification.get('data.username2');
|
||||
if (count===0) {
|
||||
text = I18n.t('notifications.liked_2', {description, username, username2});
|
||||
} else {
|
||||
text = I18n.t('notifications.liked_many', {description, username, username2, count});
|
||||
}
|
||||
}
|
||||
else {
|
||||
text = I18n.t(this.get('scope'), {description, username});
|
||||
}
|
||||
text = Discourse.Emoji.unescape(text);
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':gap', ':jagged-border', 'gap::hidden'],
|
||||
|
||||
initGaps: function(){
|
||||
this.set('loading', false);
|
||||
const before = this.get('before') === 'true';
|
||||
const gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after');
|
||||
|
||||
if (gaps) {
|
||||
this.set('gap', gaps[this.get('post.id')]);
|
||||
}
|
||||
}.on('init'),
|
||||
|
||||
gapsChanged: function(){
|
||||
this.initGaps();
|
||||
this.rerender();
|
||||
}.observes('post.hasGap'),
|
||||
|
||||
render(buffer) {
|
||||
if (this.get('loading')) {
|
||||
buffer.push(I18n.t('loading'));
|
||||
} else {
|
||||
const gapLength = this.get('gap.length');
|
||||
if (gapLength) {
|
||||
buffer.push(I18n.t('post.gap', {count: gapLength}));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
if (this.get('loading') || (!this.get('gap'))) { return false; }
|
||||
this.set('loading', true);
|
||||
this.rerender();
|
||||
|
||||
const postStream = this.get('postStream');
|
||||
const filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter;
|
||||
|
||||
filler.call(postStream, this.get('post'), this.get('gap')).then(() => {
|
||||
this.set('gap', null);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -1,85 +0,0 @@
|
||||
const MAX_SHOWN = 5;
|
||||
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const { get, isEmpty, Component } = Ember;
|
||||
|
||||
export default Component.extend(StringBuffer, {
|
||||
classNameBindings: [':gutter'],
|
||||
|
||||
rerenderTriggers: ['expanded'],
|
||||
|
||||
// Roll up links to avoid duplicates
|
||||
@computed('links')
|
||||
collapsed(links) {
|
||||
const seen = {};
|
||||
const result = [];
|
||||
|
||||
if (!isEmpty(links)) {
|
||||
links.forEach(function(l) {
|
||||
const title = get(l, 'title');
|
||||
if (!seen[title]) {
|
||||
result.pushObject(l);
|
||||
seen[title] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
renderString(buffer) {
|
||||
const links = this.get('collapsed');
|
||||
const collapsed = !this.get('expanded');
|
||||
|
||||
if (!isEmpty(links)) {
|
||||
let toRender = links;
|
||||
if (collapsed) {
|
||||
toRender = toRender.slice(0, MAX_SHOWN);
|
||||
}
|
||||
|
||||
buffer.push("<ul class='post-links'>");
|
||||
toRender.forEach(function(l) {
|
||||
const direction = get(l, 'reflection') ? 'inbound' : 'outbound',
|
||||
clicks = get(l, 'clicks');
|
||||
|
||||
buffer.push(`<li><a href='${get(l, 'url')}' class='track-link ${direction}'>`);
|
||||
|
||||
let title = get(l, 'title');
|
||||
if (!isEmpty(title)) {
|
||||
title = Discourse.Utilities.escapeExpression(title);
|
||||
buffer.push(Discourse.Emoji.unescape(title));
|
||||
}
|
||||
if (clicks) {
|
||||
buffer.push(`<span class='badge badge-notification clicks'>${clicks}</span>`);
|
||||
}
|
||||
buffer.push("</a></li>");
|
||||
});
|
||||
|
||||
if (collapsed) {
|
||||
const remaining = links.length - MAX_SHOWN;
|
||||
if (remaining > 0) {
|
||||
buffer.push(`<li><a href class='toggle-more'>${I18n.t('post.more_links', {count: remaining})}</a></li>`);
|
||||
}
|
||||
}
|
||||
buffer.push('</ul>');
|
||||
}
|
||||
|
||||
if (this.get('canReplyAsNewTopic')) {
|
||||
buffer.push(`<a href class='reply-new'>${iconHTML('plus')}${I18n.t('post.reply_as_new_topic')}</a>`);
|
||||
}
|
||||
},
|
||||
|
||||
click(e) {
|
||||
const $target = $(e.target);
|
||||
if ($target.hasClass('toggle-more')) {
|
||||
this.toggleProperty('expanded');
|
||||
return false;
|
||||
} else if ($target.closest('.reply-new').length) {
|
||||
this.sendAction('newTopicAction', this.get('post'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -1,440 +0,0 @@
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
|
||||
// Helper class for rendering a button
|
||||
export const Button = function(action, label, icon, opts) {
|
||||
this.action = action;
|
||||
this.label = label;
|
||||
|
||||
if (typeof icon === "object") {
|
||||
this.opts = icon;
|
||||
} else {
|
||||
this.icon = icon;
|
||||
}
|
||||
this.opts = this.opts || opts || {};
|
||||
};
|
||||
|
||||
function animateHeart($elem, start, end, complete) {
|
||||
if (Ember.testing) { return Ember.run(this, complete); }
|
||||
|
||||
$elem.stop()
|
||||
.css('textIndent', start)
|
||||
.animate({ textIndent: end }, {
|
||||
complete,
|
||||
step(now) {
|
||||
$(this).css('transform','scale('+now+')');
|
||||
},
|
||||
duration: 150
|
||||
}, 'linear');
|
||||
}
|
||||
|
||||
Button.prototype.render = function(buffer) {
|
||||
const opts = this.opts;
|
||||
|
||||
const label = I18n.t(this.label, opts.labelOptions);
|
||||
if (opts.prefixHTML) {
|
||||
buffer.push(opts.prefixHTML);
|
||||
}
|
||||
buffer.push("<button aria-label=\"" + label +"\" " + "title=\"" + label + "\"");
|
||||
|
||||
if (opts.disabled) { buffer.push(" disabled"); }
|
||||
if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
|
||||
if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
|
||||
if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
|
||||
buffer.push(" data-action=\"" + this.action + "\">");
|
||||
if (this.icon) { buffer.push(iconHTML(this.icon)); }
|
||||
if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
|
||||
if (opts.innerHTML) { buffer.push(opts.innerHTML); }
|
||||
buffer.push("</button>");
|
||||
};
|
||||
|
||||
let hiddenButtons;
|
||||
|
||||
const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
||||
tagName: 'section',
|
||||
classNames: ['post-menu-area', 'clearfix'],
|
||||
|
||||
rerenderTriggers: [
|
||||
'post.deleted_at',
|
||||
'post.likeAction.count',
|
||||
'post.likeAction.users.length',
|
||||
'post.reply_count',
|
||||
'post.showRepliesBelow',
|
||||
'post.can_delete',
|
||||
'post.bookmarked',
|
||||
'post.shareUrl',
|
||||
'post.topic.deleted_at',
|
||||
'post.replies.length',
|
||||
'post.wiki',
|
||||
'post.post_type',
|
||||
'collapsed'],
|
||||
|
||||
_collapsedByDefault: function() {
|
||||
this.set('collapsed', true);
|
||||
}.on('init'),
|
||||
|
||||
renderString(buffer) {
|
||||
const post = this.get('post');
|
||||
|
||||
buffer.push("<nav class='post-controls'>");
|
||||
this.renderReplies(post, buffer);
|
||||
this.renderButtons(post, buffer);
|
||||
this.renderAdminPopup(post, buffer);
|
||||
buffer.push("</nav>");
|
||||
},
|
||||
|
||||
// Delegate click actions
|
||||
click(e) {
|
||||
const $target = $(e.target);
|
||||
const action = $target.data('action') || $target.parent().data('action');
|
||||
|
||||
if ($target.prop('disabled') || $target.parent().prop('disabled')) { return; }
|
||||
|
||||
if (!action) return;
|
||||
const handler = this["click" + action.classify()];
|
||||
if (!handler) return;
|
||||
|
||||
handler.call(this, this.get('post'));
|
||||
},
|
||||
|
||||
// Replies Button
|
||||
renderReplies(post, buffer) {
|
||||
if (!post.get('showRepliesBelow')) return;
|
||||
|
||||
const replyCount = post.get('reply_count');
|
||||
buffer.push("<button class='show-replies highlight-action' data-action='replies'>");
|
||||
buffer.push(I18n.t("post.has_replies", { count: replyCount || 0 }));
|
||||
|
||||
const icon = (this.get('post.replies.length') > 0) ? 'chevron-up' : 'chevron-down';
|
||||
return buffer.push(iconHTML(icon) + "</button>");
|
||||
},
|
||||
|
||||
renderButtons(post, buffer) {
|
||||
const self = this;
|
||||
const allButtons = [];
|
||||
let visibleButtons = [];
|
||||
|
||||
if (typeof hiddenButtons === "undefined") {
|
||||
if (!Em.isEmpty(this.siteSettings.post_menu_hidden_items)) {
|
||||
hiddenButtons = this.siteSettings.post_menu_hidden_items.split('|');
|
||||
} else {
|
||||
hiddenButtons = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (post.get("bookmarked")) {
|
||||
hiddenButtons.removeObject("bookmark");
|
||||
}
|
||||
|
||||
const yours = post.get('yours');
|
||||
this.siteSettings.post_menu.split("|").forEach(function(i) {
|
||||
const creator = self["buttonFor" + i.classify()];
|
||||
if (creator) {
|
||||
const button = creator.call(self, post);
|
||||
if (button) {
|
||||
allButtons.push(button);
|
||||
if ((yours && button.opts.alwaysShowYours) ||
|
||||
(post.get('wiki') && button.opts.alwaysShowWiki) ||
|
||||
(hiddenButtons.indexOf(i) === -1)) {
|
||||
visibleButtons.push(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Only show ellipsis if there is more than one button hidden
|
||||
// if there are no more buttons, we are not collapsed
|
||||
const collapsed = this.get('collapsed');
|
||||
if (!collapsed || (allButtons.length <= visibleButtons.length + 1)) {
|
||||
visibleButtons = allButtons;
|
||||
if (collapsed) { this.set('collapsed', false); }
|
||||
} else {
|
||||
visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
|
||||
}
|
||||
|
||||
const callbacks = PostMenuComponent._registerButtonCallbacks;
|
||||
if (callbacks) {
|
||||
_.each(callbacks, function(callback) {
|
||||
callback.apply(self, [visibleButtons]);
|
||||
});
|
||||
}
|
||||
|
||||
buffer.push('<div class="actions">');
|
||||
visibleButtons.forEach((b) => b.render(buffer));
|
||||
buffer.push("</div>");
|
||||
},
|
||||
|
||||
clickLikeCount() {
|
||||
this.sendActionTarget('toggleWhoLiked');
|
||||
},
|
||||
|
||||
sendActionTarget(action, arg) {
|
||||
const target = this.get(`${action}Target`);
|
||||
return target ? target.send(this.get(action), arg) : this.sendAction(action, arg);
|
||||
},
|
||||
|
||||
clickReplies() {
|
||||
if (this.get('post.replies.length') > 0) {
|
||||
this.set('post.replies', []);
|
||||
} else {
|
||||
this.get('post').loadReplies();
|
||||
}
|
||||
},
|
||||
|
||||
// Delete button
|
||||
buttonForDelete(post) {
|
||||
let label, icon;
|
||||
|
||||
if (post.get('post_number') === 1) {
|
||||
// If it's the first post, the delete/undo actions are related to the topic
|
||||
const topic = post.get('topic');
|
||||
if (topic.get('deleted_at')) {
|
||||
if (!topic.get('details.can_recover')) { return; }
|
||||
label = "topic.actions.recover";
|
||||
icon = "undo";
|
||||
} else {
|
||||
if (!topic.get('details.can_delete')) { return; }
|
||||
label = "topic.actions.delete";
|
||||
icon = "trash-o";
|
||||
}
|
||||
|
||||
} else {
|
||||
// The delete actions target the post iteself
|
||||
if (post.get('deleted_at') || post.get('user_deleted')) {
|
||||
if (!post.get('can_recover')) { return; }
|
||||
label = "post.controls.undelete";
|
||||
icon = "undo";
|
||||
} else {
|
||||
if (!post.get('can_delete')) { return; }
|
||||
label = "post.controls.delete";
|
||||
icon = "trash-o";
|
||||
}
|
||||
}
|
||||
const action = (icon === 'trash-o') ? 'delete' : 'recover';
|
||||
let opts;
|
||||
if (icon === "trash-o"){
|
||||
opts = {className: 'delete'};
|
||||
}
|
||||
return new Button(action, label, icon, opts);
|
||||
},
|
||||
|
||||
clickRecover(post) {
|
||||
this.sendAction('recoverPost', post);
|
||||
},
|
||||
|
||||
clickDelete(post) {
|
||||
this.sendAction('deletePost', post);
|
||||
},
|
||||
|
||||
// Like button
|
||||
buttonForLike() {
|
||||
const likeAction = this.get('post.likeAction');
|
||||
if (!likeAction) { return; }
|
||||
|
||||
const className = likeAction.get('acted') ? 'has-like fade-out' : 'like';
|
||||
const opts = {className: className};
|
||||
|
||||
if (likeAction.get('canToggle')) {
|
||||
const descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
|
||||
return new Button('like', descKey, 'heart', opts);
|
||||
} else if (likeAction.get('acted')) {
|
||||
opts.disabled = true;
|
||||
return new Button('like', 'post.controls.has_liked', 'heart', opts);
|
||||
}
|
||||
},
|
||||
|
||||
buttonForLikeCount() {
|
||||
const likeCount = this.get('post.likeAction.count') || 0;
|
||||
if (likeCount > 0) {
|
||||
const likedPost = !!this.get('post.likeAction.acted');
|
||||
|
||||
const label = likedPost
|
||||
? likeCount === 1 ? 'post.has_likes_title_only_you' : 'post.has_likes_title_you'
|
||||
: 'post.has_likes_title';
|
||||
|
||||
return new Button('like-count', label, undefined, {
|
||||
className: 'like-count highlight-action',
|
||||
innerHTML: I18n.t("post.has_likes", { count: likeCount }),
|
||||
labelOptions: {count: likedPost ? (likeCount-1) : likeCount}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clickLike(post) {
|
||||
const $heart = this.$('.fa-heart'),
|
||||
$likeButton = this.$('button[data-action=like]'),
|
||||
acted = post.get('likeAction.acted'),
|
||||
self = this;
|
||||
|
||||
if (acted) {
|
||||
this.sendActionTarget('toggleLike');
|
||||
$likeButton.removeClass('has-like').addClass('like');
|
||||
} else {
|
||||
const scale = [1.0, 1.5];
|
||||
animateHeart($heart, scale[0], scale[1], function() {
|
||||
animateHeart($heart, scale[1], scale[0], function() {
|
||||
self.sendActionTarget('toggleLike');
|
||||
$likeButton.removeClass('like').addClass('has-like');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Flag button
|
||||
buttonForFlag(post) {
|
||||
if (Em.isEmpty(post.get('flagsAvailable'))) return;
|
||||
return new Button('flag', 'post.controls.flag', 'flag');
|
||||
},
|
||||
|
||||
clickFlag(post) {
|
||||
this.sendAction('showFlags', post);
|
||||
},
|
||||
|
||||
// Edit button
|
||||
buttonForEdit(post) {
|
||||
if (!post.get('can_edit')) return;
|
||||
return new Button('edit', 'post.controls.edit', 'pencil', {
|
||||
alwaysShowYours: true,
|
||||
alwaysShowWiki: true
|
||||
});
|
||||
},
|
||||
|
||||
clickEdit(post) {
|
||||
this.sendAction('editPost', post);
|
||||
},
|
||||
|
||||
// Share button
|
||||
buttonForShare(post) {
|
||||
const options = {
|
||||
shareUrl: post.get('shareUrl'),
|
||||
postNumber: post.get('post_number')
|
||||
};
|
||||
return new Button('share', 'post.controls.share', 'link', options);
|
||||
},
|
||||
|
||||
// Reply button
|
||||
buttonForReply() {
|
||||
if (!this.get('canCreatePost')) return;
|
||||
const options = {className: 'create fade-out'};
|
||||
|
||||
if(!Discourse.Mobile.mobileView) {
|
||||
options.textLabel = 'topic.reply.title';
|
||||
}
|
||||
|
||||
return new Button('reply', 'post.controls.reply', 'reply', options);
|
||||
},
|
||||
|
||||
clickReply(post) {
|
||||
this.sendAction('replyToPost', post);
|
||||
},
|
||||
|
||||
// Bookmark button
|
||||
buttonForBookmark(post) {
|
||||
if (!Discourse.User.current()) return;
|
||||
|
||||
let iconClass = 'read-icon',
|
||||
buttonClass = 'bookmark',
|
||||
tooltip = 'bookmarks.not_bookmarked';
|
||||
|
||||
if (post.get('bookmarked')) {
|
||||
iconClass += ' bookmarked';
|
||||
buttonClass += ' bookmarked';
|
||||
tooltip = 'bookmarks.created';
|
||||
}
|
||||
|
||||
return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "<div class='" + iconClass + "'>"});
|
||||
},
|
||||
|
||||
clickBookmark(post) {
|
||||
this.sendAction('toggleBookmark', post);
|
||||
},
|
||||
|
||||
// Wiki button
|
||||
buttonForWiki(post) {
|
||||
if (!post.get('can_wiki')) return;
|
||||
|
||||
if (post.get('wiki')) {
|
||||
return new Button('wiki', 'post.controls.unwiki', 'pencil-square-o', {className: 'wiki wikied'});
|
||||
} else {
|
||||
return new Button('wiki', 'post.controls.wiki', 'pencil-square-o', {className: 'wiki'});
|
||||
}
|
||||
},
|
||||
|
||||
clickWiki(post) {
|
||||
this.sendAction('toggleWiki', post);
|
||||
},
|
||||
|
||||
buttonForAdmin() {
|
||||
if (!Discourse.User.currentProp('canManageTopic')) { return; }
|
||||
return new Button('admin', 'post.controls.admin', 'wrench');
|
||||
},
|
||||
|
||||
renderAdminPopup(post, buffer) {
|
||||
if (!Discourse.User.currentProp('canManageTopic')) { return; }
|
||||
|
||||
const isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
|
||||
postTypeIcon = iconHTML('shield'),
|
||||
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
|
||||
rebakePostIcon = iconHTML('cog'),
|
||||
rebakePostText = I18n.t('post.controls.rebake'),
|
||||
unhidePostIcon = iconHTML('eye'),
|
||||
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>' +
|
||||
'<ul>' +
|
||||
(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>';
|
||||
|
||||
buffer.push(html);
|
||||
},
|
||||
|
||||
clickAdmin() {
|
||||
const $postAdminMenu = this.$(".post-admin-menu");
|
||||
$postAdminMenu.show();
|
||||
$("html").on("mouseup.post-admin-menu", function() {
|
||||
$postAdminMenu.hide();
|
||||
$("html").off("mouseup.post-admin-menu");
|
||||
});
|
||||
},
|
||||
|
||||
clickTogglePostType() {
|
||||
this.sendAction("togglePostType", this.get("post"));
|
||||
},
|
||||
|
||||
clickRebakePost() {
|
||||
this.sendAction("rebakePost", this.get("post"));
|
||||
},
|
||||
|
||||
clickUnhidePost() {
|
||||
this.sendAction("unhidePost", this.get("post"));
|
||||
},
|
||||
|
||||
clickChangePostOwner() {
|
||||
this.sendAction("changePostOwner", this.get("post"));
|
||||
},
|
||||
|
||||
buttonForShowMoreActions() {
|
||||
return new Button('showMoreActions', 'show_more', 'ellipsis-h');
|
||||
},
|
||||
|
||||
clickShowMoreActions() {
|
||||
this.set('collapsed', false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
PostMenuComponent.reopenClass({
|
||||
registerButton(callback){
|
||||
this._registerButtonCallbacks = this._registerButtonCallbacks || [];
|
||||
this._registerButtonCallbacks.push(callback);
|
||||
}
|
||||
});
|
||||
|
||||
export default PostMenuComponent;
|
||||
@ -1,77 +1,2 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
|
||||
const PosterNameComponent = Em.Component.extend({
|
||||
classNames: ['names', 'trigger-user-card'],
|
||||
displayNameOnPosts: setting('display_name_on_posts'),
|
||||
|
||||
// sanitize name for comparison
|
||||
sanitizeName(name){
|
||||
return name.toLowerCase().replace(/[\s_-]/g,'');
|
||||
},
|
||||
|
||||
render(buffer) {
|
||||
const post = this.get('post');
|
||||
|
||||
if (post) {
|
||||
const username = post.get('username'),
|
||||
primaryGroupName = post.get('primary_group_name'),
|
||||
url = post.get('usernameUrl');
|
||||
|
||||
var linkClass = 'username',
|
||||
name = post.get('name');
|
||||
|
||||
if (post.get('staff')) { linkClass += ' staff'; }
|
||||
if (post.get('admin')) { linkClass += ' admin'; }
|
||||
if (post.get('moderator')) { linkClass += ' moderator'; }
|
||||
if (post.get('new_user')) { linkClass += ' new-user'; }
|
||||
|
||||
if (!Em.isEmpty(primaryGroupName)) {
|
||||
linkClass += ' ' + primaryGroupName;
|
||||
}
|
||||
// Main link
|
||||
buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true' data-user-card='" + username + "'>" + username + "</a>");
|
||||
|
||||
// Add a glyph if we have one
|
||||
const glyph = this.posterGlyph(post);
|
||||
if (!Em.isEmpty(glyph)) {
|
||||
buffer.push(glyph);
|
||||
}
|
||||
buffer.push("</span>");
|
||||
|
||||
// Are we showing full names?
|
||||
if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) {
|
||||
name = Discourse.Utilities.escapeExpression(name);
|
||||
buffer.push("<span class='full-name'><a href='" + url + "' data-auto-route='true' data-user-card='" + username + "'>" + name + "</a></span>");
|
||||
}
|
||||
|
||||
// User titles
|
||||
let title = post.get('user_title');
|
||||
if (!Em.isEmpty(title)) {
|
||||
|
||||
title = Discourse.Utilities.escapeExpression(title);
|
||||
buffer.push('<span class="user-title">');
|
||||
if (Em.isEmpty(primaryGroupName)) {
|
||||
buffer.push(title);
|
||||
} else {
|
||||
buffer.push("<a href='/groups/" + post.get('primary_group_name') + "' class='user-group'>" + title + "</a>");
|
||||
}
|
||||
buffer.push("</span>");
|
||||
}
|
||||
|
||||
PosterNameComponent.trigger('renderedName', buffer, post);
|
||||
}
|
||||
},
|
||||
|
||||
// Overwrite this to give a user a custom font awesome glyph.
|
||||
posterGlyph(post) {
|
||||
if(post.get('moderator')) {
|
||||
const desc = I18n.t('user.moderator_tooltip');
|
||||
return '<i class="fa fa-shield" title="' + desc + '" alt="' + desc + '"></i>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Support for event triggering
|
||||
PosterNameComponent.reopenClass(Em.Evented);
|
||||
|
||||
export default PosterNameComponent;
|
||||
const removed = new Discourse.RemovedObject('discourse/components/poster-name');
|
||||
export default removed;
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
layoutName: 'components/private-message-map',
|
||||
tagName: 'section',
|
||||
classNames: ['information'],
|
||||
details: Em.computed.alias('topic.details'),
|
||||
|
||||
actions: {
|
||||
removeAllowedUser: function(user) {
|
||||
var self = this;
|
||||
bootbox.dialog(I18n.t("private_message_info.remove_allowed_user", {name: user.get('username')}), [
|
||||
{label: I18n.t("no_value"),
|
||||
'class': 'btn-danger right'},
|
||||
{label: I18n.t("yes_value"),
|
||||
'class': 'btn-primary',
|
||||
callback: function() {
|
||||
self.get('topic.details').removeAllowedUser(user);
|
||||
}
|
||||
}
|
||||
]);
|
||||
},
|
||||
|
||||
showPrivateInvite: function() {
|
||||
this.sendAction('showPrivateInviteAction');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,201 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||
|
||||
function findTopView($posts, viewportTop, min, max) {
|
||||
if (max < min) { return min; }
|
||||
|
||||
while(max>min){
|
||||
const mid = Math.floor((min + max) / 2);
|
||||
const $post = $($posts[mid]);
|
||||
const viewBottom = $post.position().top + $post.height();
|
||||
|
||||
if (viewBottom > viewportTop) {
|
||||
max = mid-1;
|
||||
} else {
|
||||
min = mid+1;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
export default MountWidget.extend({
|
||||
widget: 'post-stream',
|
||||
_topVisible: null,
|
||||
_bottomVisible: null,
|
||||
|
||||
args: Ember.computed(function() {
|
||||
return this.getProperties('posts',
|
||||
'canCreatePost',
|
||||
'multiSelect',
|
||||
'gaps',
|
||||
'selectedQuery',
|
||||
'selectedPostsCount',
|
||||
'searchService');
|
||||
}).volatile(),
|
||||
|
||||
scrolled() {
|
||||
if (this.isDestroyed || this.isDestroying) { return; }
|
||||
|
||||
const $w = $(window);
|
||||
const windowHeight = window.innerHeight ? window.innerHeight : $w.height();
|
||||
const slack = Math.round(windowHeight * 5);
|
||||
const onscreen = [];
|
||||
const nearby = [];
|
||||
|
||||
let windowTop = $w.scrollTop();
|
||||
|
||||
const $posts = this.$('.onscreen-post, .cloaked-post');
|
||||
const viewportTop = windowTop - slack;
|
||||
const topView = findTopView($posts, viewportTop, 0, $posts.length-1);
|
||||
|
||||
let windowBottom = windowTop + windowHeight;
|
||||
let viewportBottom = windowBottom + slack;
|
||||
|
||||
const bodyHeight = $('body').height();
|
||||
if (windowBottom > bodyHeight) { windowBottom = bodyHeight; }
|
||||
if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; }
|
||||
|
||||
let bottomView = topView;
|
||||
while (bottomView < $posts.length) {
|
||||
const post = $posts[bottomView];
|
||||
const $post = $(post);
|
||||
|
||||
if (!$post) { break; }
|
||||
|
||||
const viewTop = $post.offset().top;
|
||||
const viewBottom = viewTop + $post.height() + 100;
|
||||
|
||||
if (viewTop > viewportBottom) { break; }
|
||||
|
||||
if (viewBottom > windowTop && viewTop <= windowBottom) {
|
||||
onscreen.push(bottomView);
|
||||
}
|
||||
nearby.push(bottomView);
|
||||
|
||||
bottomView++;
|
||||
}
|
||||
|
||||
const posts = this.posts;
|
||||
const refresh = cb => this.queueRerender(cb);
|
||||
if (onscreen.length) {
|
||||
const first = posts.objectAt(onscreen[0]);
|
||||
if (this._topVisible !== first) {
|
||||
this._topVisible = first;
|
||||
const $body = $('body');
|
||||
const elem = $posts[onscreen[0]];
|
||||
const elemId = elem.id;
|
||||
const $elem = $(elem);
|
||||
const elemPos = $elem.position();
|
||||
const distToElement = elemPos ? $body.scrollTop() - elemPos.top : 0;
|
||||
|
||||
const topRefresh = () => {
|
||||
refresh(() => {
|
||||
const $refreshedElem = $(`#${elemId}`);
|
||||
|
||||
// Quickly going back might mean the element is destroyed
|
||||
const position = $refreshedElem.position();
|
||||
if (position && position.top) {
|
||||
$('html, body').scrollTop(position.top + distToElement);
|
||||
}
|
||||
});
|
||||
};
|
||||
this.sendAction('topVisibleChanged', { post: first, refresh: topRefresh });
|
||||
}
|
||||
|
||||
const last = posts.objectAt(onscreen[onscreen.length-1]);
|
||||
if (this._bottomVisible !== last) {
|
||||
this._bottomVisible = last;
|
||||
this.sendAction('bottomVisibleChanged', { post: last, refresh });
|
||||
}
|
||||
} else {
|
||||
this._topVisible = null;
|
||||
this._bottomVisible = null;
|
||||
}
|
||||
|
||||
const onscreenPostNumbers = [];
|
||||
const prev = this._previouslyNearby;
|
||||
const newPrev = {};
|
||||
nearby.forEach(idx => {
|
||||
const post = posts.objectAt(idx);
|
||||
const postNumber = post.post_number;
|
||||
delete prev[postNumber];
|
||||
|
||||
if (onscreen.indexOf(idx) !== -1) {
|
||||
onscreenPostNumbers.push(postNumber);
|
||||
}
|
||||
newPrev[postNumber] = post;
|
||||
uncloak(post, this);
|
||||
});
|
||||
|
||||
Object.keys(prev).forEach(pn => cloak(prev[pn], this));
|
||||
|
||||
this._previouslyNearby = newPrev;
|
||||
this.screenTrack.setOnscreen(onscreenPostNumbers);
|
||||
},
|
||||
|
||||
_scrollTriggered() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this.scrolled);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
const debouncedScroll = () => Ember.run.debounce(this, this._scrollTriggered, 10);
|
||||
|
||||
this._previouslyNearby = {};
|
||||
|
||||
this.appEvents.on('post-stream:refresh', debouncedScroll);
|
||||
$(document).bind('touchmove.post-stream', debouncedScroll);
|
||||
$(window).bind('scroll.post-stream', debouncedScroll);
|
||||
this._scrollTriggered();
|
||||
|
||||
this.appEvents.on('post-stream:posted', staged => {
|
||||
const disableJumpReply = this.currentUser.get('disable_jump_reply');
|
||||
|
||||
this.queueRerender(() => {
|
||||
if (staged && !disableJumpReply) {
|
||||
const postNumber = staged.get('post_number');
|
||||
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.$().on('mouseenter.post-stream', 'button.widget-button', e => {
|
||||
$('button.widget-button').removeClass('d-hover');
|
||||
$(e.target).addClass('d-hover');
|
||||
});
|
||||
|
||||
this.$().on('mouseleave.post-stream', 'button.widget-button', () => {
|
||||
$('button.widget-button').removeClass('d-hover');
|
||||
});
|
||||
|
||||
this.appEvents.on('post-stream:refresh', args => {
|
||||
if (args) {
|
||||
if (args.id) {
|
||||
keyDirty(`post-${args.id}`);
|
||||
|
||||
if (args.refreshLikes) {
|
||||
keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
|
||||
}
|
||||
} else if (args.force) {
|
||||
keyDirty(`*`);
|
||||
}
|
||||
}
|
||||
this.queueRerender();
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
$(document).unbind('touchmove.post-stream');
|
||||
$(window).unbind('scroll.post-stream');
|
||||
this.appEvents.off('post-stream:refresh');
|
||||
this.$().off('mouseenter.post-stream');
|
||||
this.$().off('mouseleave.post-stream');
|
||||
this.appEvents.off('post-stream:refresh');
|
||||
this.appEvents.off('post-stream:posted');
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,33 +1,17 @@
|
||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const icons = {
|
||||
'closed.enabled': 'lock',
|
||||
'closed.disabled': 'unlock-alt',
|
||||
'autoclosed.enabled': 'lock',
|
||||
'autoclosed.disabled': 'unlock-alt',
|
||||
'archived.enabled': 'folder',
|
||||
'archived.disabled': 'folder-open',
|
||||
'pinned.enabled': 'thumb-tack',
|
||||
'pinned.disabled': 'thumb-tack unpinned',
|
||||
'pinned_globally.enabled': 'thumb-tack',
|
||||
'pinned_globally.disabled': 'thumb-tack unpinned',
|
||||
'visible.enabled': 'eye',
|
||||
'visible.disabled': 'eye-slash',
|
||||
'split_topic': 'sign-out',
|
||||
'invited_user': 'plus-circle',
|
||||
'removed_user': 'minus-circle'
|
||||
};
|
||||
export function actionDescriptionHtml(actionCode, createdAt, username) {
|
||||
const dt = new Date(createdAt);
|
||||
const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
|
||||
const who = username ? `<a class="mention" href="/users/${username}">@${username}</a>` : "";
|
||||
return I18n.t(`action_codes.${actionCode}`, { who, when }).htmlSafe();
|
||||
}
|
||||
|
||||
export function actionDescription(actionCode, createdAt, username) {
|
||||
return function() {
|
||||
const ac = this.get(actionCode);
|
||||
if (ac) {
|
||||
const dt = new Date(this.get(createdAt));
|
||||
const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
|
||||
const u = this.get(username);
|
||||
const who = u ? `<a class="mention" href="/users/${u}">@${u}</a>` : "";
|
||||
return I18n.t(`action_codes.${ac}`, { who, when }).htmlSafe();
|
||||
return actionDescriptionHtml(ac, this.get(createdAt), this.get(username));
|
||||
}
|
||||
}.property(actionCode, createdAt);
|
||||
}
|
||||
@ -38,11 +22,6 @@ export default Ember.Component.extend({
|
||||
|
||||
description: actionDescription('actionCode', 'post.created_at', 'post.action_code_who'),
|
||||
|
||||
@computed("actionCode")
|
||||
icon(actionCode) {
|
||||
return icons[actionCode] || 'exclamation';
|
||||
},
|
||||
|
||||
actions: {
|
||||
edit() {
|
||||
this.sendAction('editPost', this.get('post'));
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import SmallActionComponent from 'discourse/components/small-action';
|
||||
|
||||
export default SmallActionComponent.extend({
|
||||
classNames: ['time-gap'],
|
||||
classNameBindings: ['hideTimeGap::hidden'],
|
||||
hideTimeGap: Em.computed.alias('postStream.hasNoFilters'),
|
||||
icon: 'clock-o',
|
||||
|
||||
description: function() {
|
||||
const gapDays = this.get('daysAgo');
|
||||
if (gapDays < 30) {
|
||||
return I18n.t('dates.later.x_days', {count: gapDays});
|
||||
} else if (gapDays < 365) {
|
||||
const gapMonths = Math.floor(gapDays / 30);
|
||||
return I18n.t('dates.later.x_months', {count: gapMonths});
|
||||
} else {
|
||||
const gapYears = Math.floor(gapDays / 365);
|
||||
return I18n.t('dates.later.x_years', {count: gapYears});
|
||||
}
|
||||
}.property(),
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
layoutName: 'components/toggle-summary',
|
||||
tagName: 'section',
|
||||
classNames: ['information'],
|
||||
postStream: Em.computed.alias('topic.postStream'),
|
||||
|
||||
actions: {
|
||||
toggleSummary() {
|
||||
this.get('postStream').toggleSummary();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -19,7 +19,7 @@ export default Ember.Component.extend({
|
||||
}.property(),
|
||||
|
||||
skipHeader: function() {
|
||||
return Discourse.Mobile.mobileView;
|
||||
return this.site.mobileView;
|
||||
}.property(),
|
||||
|
||||
showLikes: function(){
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
var LINKS_SHOWN = 5;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
mapCollapsed: true,
|
||||
layoutName: 'components/topic-map',
|
||||
details: Em.computed.alias('topic.details'),
|
||||
allLinksShown: false,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
|
||||
// If the topic has a summary, expand the map by default
|
||||
this.set('mapCollapsed', Discourse.Mobile.mobileView || (!this.get('topic.has_summary')));
|
||||
},
|
||||
|
||||
showPosterAvatar: Em.computed.gt('topic.posts_count', 2),
|
||||
|
||||
toggleMapClass: function() {
|
||||
return this.get('mapCollapsed') ? 'chevron-down' : 'chevron-up';
|
||||
}.property('mapCollapsed'),
|
||||
|
||||
showAllLinksControls: function() {
|
||||
if (this.get('allLinksShown')) return false;
|
||||
if ((this.get('details.links.length') || 0) <= LINKS_SHOWN) return false;
|
||||
return true;
|
||||
}.property('allLinksShown', 'topic.details.links'),
|
||||
|
||||
infoLinks: function() {
|
||||
var allLinks = this.get('details.links');
|
||||
if (Em.isNone(allLinks)) return [];
|
||||
|
||||
if (this.get('allLinksShown')) return allLinks;
|
||||
return allLinks.slice(0, LINKS_SHOWN);
|
||||
|
||||
}.property('details.links', 'allLinksShown'),
|
||||
|
||||
actions: {
|
||||
toggleMap: function() {
|
||||
this.toggleProperty('mapCollapsed');
|
||||
},
|
||||
|
||||
showAllLinks: function() {
|
||||
this.set('allLinksShown', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
|
||||
postStream: Em.computed.alias('participant.topic.postStream'),
|
||||
showPostCount: Em.computed.gte('participant.post_count', 2),
|
||||
|
||||
toggled: function() {
|
||||
return this.get('postStream.userFilters').contains(this.get('participant.username'));
|
||||
}.property('postStream.userFilters.[]'),
|
||||
|
||||
actions: {
|
||||
toggle() {
|
||||
const postStream = this.get('postStream');
|
||||
if (postStream) {
|
||||
postStream.toggleParticipant(this.get('participant.username'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
|
||||
export default Ember.Component.extend(StringBuffer, {
|
||||
rerenderTriggers: ['users.length'],
|
||||
|
||||
renderString(buffer) {
|
||||
const users = this.get('users');
|
||||
if (users && users.get('length') > 0) {
|
||||
buffer.push("<div class='who-liked'>");
|
||||
let iconsHtml = "";
|
||||
users.forEach(function(u) {
|
||||
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username') + "\">";
|
||||
iconsHtml += Discourse.Utilities.avatarImg({
|
||||
size: 'small',
|
||||
avatarTemplate: u.get('avatar_template'),
|
||||
title: u.get('username')
|
||||
});
|
||||
iconsHtml += "</a>";
|
||||
});
|
||||
buffer.push(I18n.t('post.actions.people.like',{icons: iconsHtml}));
|
||||
buffer.push("</div>");
|
||||
} else {
|
||||
buffer.push("<span></span>");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -57,7 +57,25 @@ export default Ember.Controller.extend({
|
||||
lastValidatedAt: null,
|
||||
isUploading: false,
|
||||
topic: null,
|
||||
showToolbar: false,
|
||||
showToolbar: Em.computed({
|
||||
get(){
|
||||
const keyValueStore = this.container.lookup('key-value-store:main');
|
||||
const storedVal = keyValueStore.get("toolbar-enabled");
|
||||
if (this._toolbarEnabled === undefined && storedVal === undefined) {
|
||||
// iPhone 6 is 375, anything narrower and toolbar should
|
||||
// be default disabled.
|
||||
// That said we should remember the state
|
||||
this._toolbarEnabled = $(window).width() > 370;
|
||||
}
|
||||
return this._toolbarEnabled || storedVal === "true";
|
||||
},
|
||||
set(key, val){
|
||||
const keyValueStore = this.container.lookup('key-value-store:main');
|
||||
this._toolbarEnabled = val;
|
||||
keyValueStore.set({key: "toolbar-enabled", value: val ? "true" : "false"});
|
||||
return val;
|
||||
}
|
||||
}),
|
||||
|
||||
_initializeSimilar: function() {
|
||||
this.set('similarTopics', []);
|
||||
@ -263,7 +281,6 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
var staged = false;
|
||||
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
||||
|
||||
// TODO: This should not happen in model
|
||||
const imageSizes = {};
|
||||
@ -281,6 +298,7 @@ export default Ember.Controller.extend({
|
||||
self.send('postWasEnqueued', result.responseJson);
|
||||
self.destroyDraft();
|
||||
self.close();
|
||||
self.appEvents.trigger('post-stream:refresh');
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -288,7 +306,15 @@ export default Ember.Controller.extend({
|
||||
if (result.responseJson.action === "create_post" || self.get('replyAsNewTopicDraft')) {
|
||||
self.destroyDraft();
|
||||
}
|
||||
if (self.get('model.action') === 'edit') {
|
||||
self.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) });
|
||||
} else {
|
||||
self.appEvents.trigger('post-stream:refresh');
|
||||
}
|
||||
|
||||
if (result.responseJson.action === "create_post") {
|
||||
self.appEvents.trigger('post:highlight', result.payload.post_number);
|
||||
}
|
||||
self.close();
|
||||
|
||||
const currentUser = Discourse.User.current();
|
||||
@ -298,14 +324,14 @@ export default Ember.Controller.extend({
|
||||
currentUser.set('reply_count', currentUser.get('reply_count') + 1);
|
||||
}
|
||||
|
||||
// TODO disableJumpReply is super crude, it needs to provide some sort
|
||||
// of notification to the end user
|
||||
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
||||
if (!composer.get('replyingToTopic') || !disableJumpReply) {
|
||||
const post = result.target;
|
||||
if (post && !staged) {
|
||||
DiscourseURL.routeTo(post.get('url'));
|
||||
}
|
||||
}
|
||||
|
||||
}).catch(function(error) {
|
||||
composer.set('disableDrafts', false);
|
||||
self.appEvents.one('composer:opened', () => bootbox.alert(error));
|
||||
@ -316,18 +342,10 @@ export default Ember.Controller.extend({
|
||||
staged = composer.get('stagedPost');
|
||||
}
|
||||
|
||||
Em.run.schedule('afterRender', function() {
|
||||
if (staged && !disableJumpReply) {
|
||||
const postNumber = staged.get('post_number');
|
||||
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
||||
self.appEvents.trigger('post:highlight', postNumber);
|
||||
}
|
||||
});
|
||||
this.appEvents.trigger('post-stream:posted', staged);
|
||||
|
||||
this.messageBus.pause();
|
||||
promise.finally(function(){
|
||||
self.messageBus.resume();
|
||||
});
|
||||
promise.finally(() => this.messageBus.resume());
|
||||
|
||||
return promise;
|
||||
},
|
||||
@ -587,14 +605,6 @@ export default Ember.Controller.extend({
|
||||
$('.d-editor-input').autocomplete({ cancel: true });
|
||||
},
|
||||
|
||||
showOptions() {
|
||||
var _ref;
|
||||
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
|
||||
archetype: this.get('model.archetype'),
|
||||
metaData: this.get('model.metaData')
|
||||
})) : void 0;
|
||||
},
|
||||
|
||||
canEdit: function() {
|
||||
return this.get("model.action") === "edit" && Discourse.User.current().get("can_edit");
|
||||
}.property("model.action"),
|
||||
|
||||
@ -16,6 +16,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
rejectedPasswords: Em.A([]),
|
||||
prefilledUsername: null,
|
||||
userFields: null,
|
||||
isDeveloper: false,
|
||||
|
||||
hasAuthOptions: Em.computed.notEmpty('authOptions'),
|
||||
canCreateLocal: setting('enable_local_logins'),
|
||||
@ -37,6 +38,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
rejectedEmails: [],
|
||||
rejectedPasswords: [],
|
||||
prefilledUsername: null,
|
||||
isDeveloper: false
|
||||
});
|
||||
this._createUserFields();
|
||||
},
|
||||
@ -70,8 +72,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
}.property('authOptions.auth_provider'),
|
||||
|
||||
passwordInstructions: function() {
|
||||
return I18n.t('user.password.instructions', {count: Discourse.SiteSettings.min_password_length});
|
||||
}.property(),
|
||||
return this.get('isDeveloper') ? I18n.t('user.password.instructions', {count: Discourse.SiteSettings.min_admin_password_length}) : I18n.t('user.password.instructions', {count: Discourse.SiteSettings.min_password_length});
|
||||
}.property('isDeveloper'),
|
||||
|
||||
nameInstructions: function() {
|
||||
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
@ -228,41 +230,27 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
const _this = this;
|
||||
if (this.shouldCheckUsernameMatch()) {
|
||||
return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(function(result) {
|
||||
_this.set('globalNicknameExists', false);
|
||||
_this.set('isDeveloper', false);
|
||||
if (result.available) {
|
||||
if (result.global_match) {
|
||||
_this.set('globalNicknameExists', true);
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
ok: true,
|
||||
reason: I18n.t('user.username.global_match')
|
||||
}));
|
||||
} else {
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
ok: true,
|
||||
reason: I18n.t('user.username.available')
|
||||
}));
|
||||
if (result.is_developer) {
|
||||
_this.set('isDeveloper', true);
|
||||
}
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
ok: true,
|
||||
reason: I18n.t('user.username.available')
|
||||
}));
|
||||
} else {
|
||||
if (result.suggestion) {
|
||||
if (result.global_match !== void 0 && result.global_match === false) {
|
||||
_this.set('globalNicknameExists', true);
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.username.global_mismatch', result)
|
||||
}));
|
||||
} else {
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.username.not_available', result)
|
||||
}));
|
||||
}
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.username.not_available', result)
|
||||
}));
|
||||
} else if (result.errors) {
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: result.errors.join(' ')
|
||||
}));
|
||||
} else {
|
||||
_this.set('globalNicknameExists', true);
|
||||
return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.username.enter_email')
|
||||
@ -297,7 +285,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
}
|
||||
|
||||
// If too short
|
||||
if (password.length < Discourse.SiteSettings.min_password_length) {
|
||||
const passwordLength = this.get('isDeveloper') ? Discourse.SiteSettings.min_admin_password_length : Discourse.SiteSettings.min_password_length;
|
||||
if (password.length < passwordLength) {
|
||||
return Discourse.InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.password.too_short')
|
||||
@ -330,7 +319,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
ok: true,
|
||||
reason: I18n.t('user.password.ok')
|
||||
});
|
||||
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail'),
|
||||
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail', 'isDeveloper'),
|
||||
|
||||
@on('init')
|
||||
fetchConfirmationValue() {
|
||||
@ -360,6 +349,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
this.set('formSubmitted', true);
|
||||
return Discourse.User.createAccount(attrs).then(function(result) {
|
||||
self.set('isDeveloper', false);
|
||||
if (result.success) {
|
||||
// Trigger the browser's password manager using the hidden static login form:
|
||||
const $hidden_login_form = $('#hidden-login-form');
|
||||
@ -369,6 +359,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
$hidden_login_form.submit();
|
||||
} else {
|
||||
self.flash(result.message || I18n.t('create_account.failed'), 'error');
|
||||
if (result.is_developer) {
|
||||
self.set('isDeveloper', true);
|
||||
}
|
||||
if (result.errors && result.errors.email && result.errors.email.length > 0 && result.values) {
|
||||
self.get('rejectedEmails').pushObject(result.values.email);
|
||||
}
|
||||
|
||||
@ -89,7 +89,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
},
|
||||
|
||||
createFlag(opts) {
|
||||
const self = this;
|
||||
let postAction; // an instance of ActionSummary
|
||||
|
||||
if (!this.get('flagTopic')) {
|
||||
@ -103,13 +102,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
this.send('hideModal');
|
||||
|
||||
postAction.act(this.get('model'), params).then(function() {
|
||||
self.send('closeModal');
|
||||
postAction.act(this.get('model'), params).then(() => {
|
||||
this.send('closeModal');
|
||||
if (params.message) {
|
||||
self.set('message', '');
|
||||
this.set('message', '');
|
||||
}
|
||||
}, function(errors) {
|
||||
self.send('closeModal');
|
||||
this.appEvents.trigger('post-stream:refresh', { id: this.get('model.id') });
|
||||
}).catch(errors => {
|
||||
this.send('closeModal');
|
||||
if (errors && errors.responseText) {
|
||||
bootbox.alert($.parseJSON(errors.responseText).errors);
|
||||
} else {
|
||||
|
||||
@ -10,7 +10,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
revisionsTextKey: "post.revisions.controls.comparing_previous_to_current_out_of_total",
|
||||
|
||||
_changeViewModeOnMobile: function() {
|
||||
if (Discourse.Mobile.mobileView) { this.set("viewMode", "inline"); }
|
||||
if (this.site.mobileView) { this.set("viewMode", "inline"); }
|
||||
}.on("init"),
|
||||
|
||||
refresh(postId, postVersion) {
|
||||
|
||||
@ -9,7 +9,6 @@ export default NavigationDefaultController.extend({
|
||||
|
||||
@computed("showingSubcategoryList", "category", "noSubcategories")
|
||||
navItems(showingSubcategoryList, category, noSubcategories) {
|
||||
if (showingSubcategoryList) { return []; }
|
||||
return Discourse.NavItem.buildList(category, { noSubcategories });
|
||||
}
|
||||
});
|
||||
|
||||
@ -68,10 +68,17 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
{name: I18n.t('user.email_previous_replies.never'), value: 2}
|
||||
],
|
||||
|
||||
digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 },
|
||||
{ name: I18n.t('user.email_digests.every_three_days'), value: 3 },
|
||||
{ name: I18n.t('user.email_digests.weekly'), value: 7 },
|
||||
{ name: I18n.t('user.email_digests.every_two_weeks'), value: 14 }],
|
||||
digestFrequencies: [{ name: I18n.t('user.email_digests.every_30_minutes'), value: 30 },
|
||||
{ name: I18n.t('user.email_digests.every_hour'), value: 60 },
|
||||
{ name: I18n.t('user.email_digests.daily'), value: 1440 },
|
||||
{ name: I18n.t('user.email_digests.every_three_days'), value: 4320 },
|
||||
{ name: I18n.t('user.email_digests.weekly'), value: 10080 },
|
||||
{ name: I18n.t('user.email_digests.every_two_weeks'), value: 20160 }],
|
||||
|
||||
likeNotificationFrequencies: [{ name: I18n.t('user.like_notification_frequency.always'), value: 0 },
|
||||
{ name: I18n.t('user.like_notification_frequency.first_time_and_daily'), value: 1 },
|
||||
{ name: I18n.t('user.like_notification_frequency.first_time'), value: 2 },
|
||||
{ name: I18n.t('user.like_notification_frequency.never'), value: 3 }],
|
||||
|
||||
autoTrackDurations: [{ name: I18n.t('user.auto_track_options.never'), value: -1 },
|
||||
{ name: I18n.t('user.auto_track_options.immediately'), value: 0 },
|
||||
|
||||
@ -61,8 +61,10 @@ export default Ember.Controller.extend({
|
||||
// containing a single invisible character
|
||||
markerElement.appendChild(document.createTextNode("\ufeff"));
|
||||
|
||||
const isMobileDevice = this.site.isMobileDevice;
|
||||
|
||||
// collapse the range at the beginning/end of the selection
|
||||
range.collapse(!Discourse.Mobile.isMobileDevice);
|
||||
range.collapse(!isMobileDevice);
|
||||
// and insert it at the start of our selection range
|
||||
range.insertNode(markerElement);
|
||||
|
||||
@ -83,7 +85,7 @@ export default Ember.Controller.extend({
|
||||
let topOff = markerOffset.top;
|
||||
let leftOff = markerOffset.left;
|
||||
|
||||
if (Discourse.Mobile.isMobileDevice) {
|
||||
if (isMobileDevice) {
|
||||
topOff = topOff + 20;
|
||||
leftOff = Math.min(leftOff + 10, $(window).width() - $quoteButton.outerWidth());
|
||||
} else {
|
||||
@ -136,7 +138,7 @@ export default Ember.Controller.extend({
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) {
|
||||
this.appEvents.trigger('composer:insert-text', quotedText.trim());
|
||||
this.appEvents.trigger('composer:insert-text', quotedText);
|
||||
} else {
|
||||
composerController.open(composerOpts);
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
|
||||
enteredAt: null,
|
||||
retrying: false,
|
||||
firstPostExpanded: false,
|
||||
adminMenuVisible: false,
|
||||
|
||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||
@ -100,7 +99,64 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
DiscourseURL.routeTo(url);
|
||||
},
|
||||
|
||||
selectedQuery: function() {
|
||||
return post => this.postSelected(post);
|
||||
}.property(),
|
||||
|
||||
actions: {
|
||||
|
||||
fillGapBefore(args) {
|
||||
return this.get('model.postStream').fillGapBefore(args.post, args.gap);
|
||||
},
|
||||
|
||||
fillGapAfter(args) {
|
||||
return this.get('model.postStream').fillGapAfter(args.post, args.gap);
|
||||
},
|
||||
|
||||
// Called the the topmost visible post on the page changes.
|
||||
topVisibleChanged(event) {
|
||||
const { post, refresh } = event;
|
||||
|
||||
if (!post) { return; }
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
const firstLoadedPost = postStream.get('posts.firstObject');
|
||||
|
||||
const currentPostNumber = post.get('post_number');
|
||||
this.set('model.currentPost', currentPostNumber);
|
||||
this.send('postChangedRoute', currentPostNumber);
|
||||
|
||||
if (post.get('post_number') === 1) { return; }
|
||||
|
||||
if (firstLoadedPost && firstLoadedPost === post) {
|
||||
postStream.prependMore().then(() => refresh());
|
||||
}
|
||||
},
|
||||
|
||||
// Called the the bottommost visible post on the page changes.
|
||||
bottomVisibleChanged(event) {
|
||||
const { post, refresh } = event;
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
const lastLoadedPost = postStream.get('posts.lastObject');
|
||||
|
||||
this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post));
|
||||
|
||||
if (lastLoadedPost && lastLoadedPost === post && postStream.get('canAppendMore')) {
|
||||
postStream.appendMore().then(() => refresh());
|
||||
// show loading stuff
|
||||
refresh();
|
||||
}
|
||||
},
|
||||
|
||||
toggleSummary() {
|
||||
return this.get('model.postStream').toggleSummary();
|
||||
},
|
||||
|
||||
removeAllowedUser(user) {
|
||||
return this.get('model.details').removeAllowedUser(user);
|
||||
},
|
||||
|
||||
showTopicAdminMenu() {
|
||||
this.set('adminMenuVisible', true);
|
||||
},
|
||||
@ -113,7 +169,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
this.deleteTopic();
|
||||
},
|
||||
|
||||
|
||||
archiveMessage() {
|
||||
const topic = this.get('model');
|
||||
topic.archiveMessage().then(()=>{
|
||||
@ -176,8 +231,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
|
||||
// Deleting the first post deletes the topic
|
||||
if (post.get('post_number') === 1) {
|
||||
this.deleteTopic();
|
||||
return;
|
||||
return this.deleteTopic();
|
||||
} else if (!post.can_delete) {
|
||||
// check if current user can delete post
|
||||
return false;
|
||||
@ -210,7 +264,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}
|
||||
]);
|
||||
} else {
|
||||
post.destroy(user).catch(function(error) {
|
||||
return post.destroy(user).then(() => {
|
||||
this.appEvents.trigger('post-stream:refresh');
|
||||
}).catch(error => {
|
||||
popupAjaxError(error);
|
||||
post.undoDeleteState();
|
||||
});
|
||||
@ -245,14 +301,17 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
},
|
||||
|
||||
toggleBookmark(post) {
|
||||
if (!Discourse.User.current()) {
|
||||
if (!this.currentUser) {
|
||||
alert(I18n.t("bookmarks.not_bookmarked"));
|
||||
return;
|
||||
}
|
||||
if (post) {
|
||||
return post.toggleBookmark().catch(popupAjaxError);
|
||||
} else {
|
||||
return this.get("model").toggleBookmark();
|
||||
return this.get("model").toggleBookmark().then(changedIds => {
|
||||
if (!changedIds) { return; }
|
||||
changedIds.forEach(id => this.appEvents.trigger('post-stream:refresh', { id }));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -261,18 +320,20 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
},
|
||||
|
||||
selectAll() {
|
||||
const posts = this.get('model.postStream.posts'),
|
||||
selectedPosts = this.get('selectedPosts');
|
||||
const posts = this.get('model.postStream.posts');
|
||||
const selectedPosts = this.get('selectedPosts');
|
||||
if (posts) {
|
||||
selectedPosts.addObjects(posts);
|
||||
}
|
||||
this.set('allPostsSelected', true);
|
||||
this.appEvents.trigger('post-stream:refresh', { force: true });
|
||||
},
|
||||
|
||||
deselectAll() {
|
||||
this.get('selectedPosts').clear();
|
||||
this.get('selectedReplies').clear();
|
||||
this.set('allPostsSelected', false);
|
||||
this.appEvents.trigger('post-stream:refresh', { force: true });
|
||||
},
|
||||
|
||||
toggleParticipant(user) {
|
||||
@ -293,6 +354,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
|
||||
toggleMultiSelect() {
|
||||
this.toggleProperty('multiSelect');
|
||||
this.appEvents.trigger('post-stream:refresh', { force: true });
|
||||
},
|
||||
|
||||
finishedEditingTopic() {
|
||||
@ -324,27 +386,26 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
},
|
||||
|
||||
deleteSelected() {
|
||||
const self = this;
|
||||
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), result => {
|
||||
if (result) {
|
||||
|
||||
// If all posts are selected, it's the same thing as deleting the topic
|
||||
if (self.get('allPostsSelected')) {
|
||||
return self.deleteTopic();
|
||||
if (this.get('allPostsSelected')) {
|
||||
return this.deleteTopic();
|
||||
}
|
||||
|
||||
const selectedPosts = self.get('selectedPosts'),
|
||||
selectedReplies = self.get('selectedReplies'),
|
||||
postStream = self.get('model.postStream'),
|
||||
toRemove = [];
|
||||
const selectedPosts = this.get('selectedPosts');
|
||||
const selectedReplies = this.get('selectedReplies');
|
||||
const postStream = this.get('model.postStream');
|
||||
|
||||
Discourse.Post.deleteMany(selectedPosts, selectedReplies);
|
||||
postStream.get('posts').forEach(function (p) {
|
||||
if (self.postSelected(p)) { toRemove.addObject(p); }
|
||||
postStream.get('posts').forEach(p => {
|
||||
if (this.postSelected(p)) {
|
||||
p.set('deleted_at', new Date());
|
||||
}
|
||||
});
|
||||
|
||||
postStream.removePosts(toRemove);
|
||||
self.send('toggleMultiSelect');
|
||||
this.send('toggleMultiSelect');
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -437,7 +498,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
composerController.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: Composer.REPLY_AS_NEW_TOPIC_KEY,
|
||||
categoryId: this.get('category.id')
|
||||
categoryId: this.get('model.category.id')
|
||||
}).then(() => {
|
||||
return Em.isEmpty(quotedText) ? "" : quotedText;
|
||||
}).then(q => {
|
||||
@ -447,18 +508,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
});
|
||||
},
|
||||
|
||||
expandFirstPost(post) {
|
||||
const self = this;
|
||||
this.set('loadingExpanded', true);
|
||||
post.expand().then(function() {
|
||||
self.set('firstPostExpanded', true);
|
||||
}).catch(function(error) {
|
||||
bootbox.alert($.parseJSON(error.responseText).errors);
|
||||
}).finally(function() {
|
||||
self.set('loadingExpanded', false);
|
||||
});
|
||||
},
|
||||
|
||||
retryLoading() {
|
||||
const self = this;
|
||||
self.set('retrying', true);
|
||||
@ -470,22 +519,22 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
},
|
||||
|
||||
toggleWiki(post) {
|
||||
post.updatePostField('wiki', !post.get('wiki'));
|
||||
return post.updatePostField('wiki', !post.get('wiki'));
|
||||
},
|
||||
|
||||
togglePostType(post) {
|
||||
const regular = this.site.get('post_types.regular');
|
||||
const moderator = this.site.get('post_types.moderator_action');
|
||||
|
||||
post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
|
||||
return post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
|
||||
},
|
||||
|
||||
rebakePost(post) {
|
||||
post.rebake();
|
||||
return post.rebake();
|
||||
},
|
||||
|
||||
unhidePost(post) {
|
||||
post.unhide();
|
||||
return post.unhide();
|
||||
},
|
||||
|
||||
changePostOwner(post) {
|
||||
@ -498,11 +547,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
this.send('togglePinnedForUser');
|
||||
},
|
||||
|
||||
showExpandButton: function() {
|
||||
const post = this.get('post');
|
||||
return post.get('post_number') === 1 && post.get('topic.expandable_first_post');
|
||||
}.property(),
|
||||
|
||||
canMergeTopic: function() {
|
||||
if (!this.get('model.details.can_move_posts')) return false;
|
||||
return this.get('selectedPostsCount') > 0;
|
||||
@ -598,9 +642,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
// Unsubscribe before subscribing again
|
||||
this.unsubscribe();
|
||||
|
||||
const self = this;
|
||||
this.messageBus.subscribe("/topic/" + this.get('model.id'), function(data) {
|
||||
const topic = self.get('model');
|
||||
const refresh = (args) => this.appEvents.trigger('post-stream:refresh', args);
|
||||
|
||||
this.messageBus.subscribe("/topic/" + this.get('model.id'), data => {
|
||||
const topic = this.get('model');
|
||||
|
||||
if (data.notification_level_change) {
|
||||
topic.set('details.notification_level', data.notification_level_change);
|
||||
@ -608,26 +653,27 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
return;
|
||||
}
|
||||
|
||||
const postStream = self.get('model.postStream');
|
||||
const postStream = this.get('model.postStream');
|
||||
switch (data.type) {
|
||||
case "revised":
|
||||
case "acted":
|
||||
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
case "revised":
|
||||
case "rebaked": {
|
||||
// TODO we could update less data for "acted" (only post actions)
|
||||
postStream.triggerChangedPost(data.id, data.updated_at);
|
||||
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id }));
|
||||
return;
|
||||
}
|
||||
case "deleted": {
|
||||
postStream.triggerDeletedPost(data.id, data.post_number);
|
||||
postStream.triggerDeletedPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
|
||||
return;
|
||||
}
|
||||
case "recovered": {
|
||||
postStream.triggerRecoveredPost(data.id, data.post_number);
|
||||
postStream.triggerRecoveredPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
|
||||
return;
|
||||
}
|
||||
case "created": {
|
||||
postStream.triggerNewPostInStream(data.id);
|
||||
if (self.get('currentUser.id') !== data.user_id) {
|
||||
postStream.triggerNewPostInStream(data.id).then(() => refresh());
|
||||
if (this.get('currentUser.id') !== data.user_id) {
|
||||
Discourse.notifyBackgroundCountIncrement();
|
||||
}
|
||||
return;
|
||||
@ -673,23 +719,17 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}
|
||||
},
|
||||
|
||||
// If our current post is changed, notify the router
|
||||
_currentPostChanged: function() {
|
||||
const currentPost = this.get('model.currentPost');
|
||||
if (currentPost) {
|
||||
this.send('postChangedRoute', currentPost);
|
||||
}
|
||||
}.observes('model.currentPost'),
|
||||
|
||||
readPosts(topicId, postNumbers) {
|
||||
const topic = this.get("model"),
|
||||
postStream = topic.get("postStream");
|
||||
const topic = this.get("model");
|
||||
const postStream = topic.get("postStream");
|
||||
|
||||
if (topic.get('id') === topicId) {
|
||||
|
||||
if (topic.get("id") === topicId) {
|
||||
// TODO identity map for postNumber
|
||||
_.each(postStream.get('posts'), post => {
|
||||
if (_.include(postNumbers, post.post_number) && !post.read) {
|
||||
post.set("read", true);
|
||||
postStream.get('posts').forEach(post => {
|
||||
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
|
||||
post.set('read', true);
|
||||
this.appEvents.trigger('post-stream:refresh', { id: post.id });
|
||||
}
|
||||
});
|
||||
|
||||
@ -709,59 +749,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}
|
||||
},
|
||||
|
||||
// Called the the topmost visible post on the page changes.
|
||||
topVisibleChanged(post) {
|
||||
if (!post) { return; }
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
const firstLoadedPost = postStream.get('posts.firstObject');
|
||||
|
||||
this.set('model.currentPost', post.get('post_number'));
|
||||
|
||||
if (post.get('post_number') === 1) { return; }
|
||||
|
||||
if (firstLoadedPost && firstLoadedPost === post) {
|
||||
// Note: jQuery shouldn't be done in a controller, but how else can we
|
||||
// trigger a scroll after a promise resolves in a controller? We need
|
||||
// to do this to preserve upwards infinte scrolling.
|
||||
const $body = $('body');
|
||||
const elemId = `#post_${post.get('post_number')}`;
|
||||
const $elem = $(elemId).closest('.post-cloak');
|
||||
const elemPos = $elem.position();
|
||||
const distToElement = elemPos ? $body.scrollTop() - elemPos.top : 0;
|
||||
|
||||
postStream.prependMore().then(function() {
|
||||
Em.run.next(function () {
|
||||
const $refreshedElem = $(elemId).closest('.post-cloak');
|
||||
|
||||
// Quickly going back might mean the element is destroyed
|
||||
const position = $refreshedElem.position();
|
||||
if (position && position.top) {
|
||||
$('html, body').scrollTop(position.top + distToElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Called the the bottommost visible post on the page changes.
|
||||
|
||||
@method bottomVisibleChanged
|
||||
@params {Discourse.Post} post that is at the bottom
|
||||
**/
|
||||
bottomVisibleChanged(post) {
|
||||
if (!post) { return; }
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
const lastLoadedPost = postStream.get('posts.lastObject');
|
||||
|
||||
this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post));
|
||||
|
||||
if (lastLoadedPost && lastLoadedPost === post) {
|
||||
postStream.appendMore();
|
||||
}
|
||||
},
|
||||
|
||||
_showFooter: function() {
|
||||
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
|
||||
|
||||
@ -45,7 +45,7 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
// Don't show on mobile
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
if (this.site.mobileView) {
|
||||
const url = "/users/" + username;
|
||||
DiscourseURL.routeTo(url);
|
||||
return;
|
||||
|
||||
@ -2,7 +2,6 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
import Topic from 'discourse/models/topic';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
needs: ["application", "user-topics-list", "user"],
|
||||
pmView: false,
|
||||
viewingSelf: Em.computed.alias('controllers.user.viewingSelf'),
|
||||
@ -11,10 +10,6 @@ export default Ember.Controller.extend({
|
||||
selected: Em.computed.alias('controllers.user-topics-list.selected'),
|
||||
bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'),
|
||||
|
||||
mobileView: function() {
|
||||
return Discourse.Mobile.mobileView;
|
||||
}.property(),
|
||||
|
||||
showNewPM: function(){
|
||||
return this.get('controllers.user.viewingSelf') &&
|
||||
Discourse.User.currentProp('can_send_private_messages');
|
||||
|
||||
@ -22,13 +22,11 @@ function loadingResolver(cb) {
|
||||
}
|
||||
|
||||
function parseName(fullName) {
|
||||
/*jshint validthis:true */
|
||||
|
||||
const nameParts = fullName.split(":"),
|
||||
type = nameParts[0], fullNameWithoutType = nameParts[1],
|
||||
name = fullNameWithoutType,
|
||||
namespace = get(this, 'namespace'),
|
||||
root = namespace;
|
||||
type = nameParts[0], fullNameWithoutType = nameParts[1],
|
||||
name = fullNameWithoutType,
|
||||
namespace = get(this, 'namespace'),
|
||||
root = namespace;
|
||||
|
||||
return {
|
||||
fullName: fullName,
|
||||
@ -85,6 +83,10 @@ export default Ember.DefaultResolver.extend({
|
||||
return module;
|
||||
},
|
||||
|
||||
resolveWidget(parsedName) {
|
||||
return this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveAdapter(parsedName) {
|
||||
return this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
@ -139,7 +141,7 @@ export default Ember.DefaultResolver.extend({
|
||||
},
|
||||
|
||||
findMobileTemplate(parsedName) {
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
if (this.mobileView) {
|
||||
var mobileParsedName = this.parseName(parsedName.fullName.replace("template:", "template:mobile/"));
|
||||
return this.findTemplate(mobileParsedName);
|
||||
}
|
||||
|
||||
8
app/assets/javascripts/discourse/helpers/as-hash.js.es6
Normal file
8
app/assets/javascripts/discourse/helpers/as-hash.js.es6
Normal file
@ -0,0 +1,8 @@
|
||||
// Note: Later versions of ember include `hash`
|
||||
export default function hashHelper(params) {
|
||||
const hash = {};
|
||||
Object.keys(params.hash).forEach(k => {
|
||||
hash[k] = params.data.view.getStream(params.hash[k]).value();
|
||||
});
|
||||
return hash;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { h } from 'virtual-dom';
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
|
||||
function iconClasses(icon, params) {
|
||||
@ -7,7 +8,7 @@ function iconClasses(icon, params) {
|
||||
return classes;
|
||||
}
|
||||
|
||||
function iconHTML(icon, params) {
|
||||
export function iconHTML(icon, params) {
|
||||
params = params || {};
|
||||
|
||||
var html = "<i class='" + iconClasses(icon, params) + "'";
|
||||
@ -19,6 +20,24 @@ function iconHTML(icon, params) {
|
||||
return html;
|
||||
}
|
||||
|
||||
export function iconNode(icon, params) {
|
||||
params = params || {};
|
||||
|
||||
const properties = {
|
||||
className: iconClasses(icon, params),
|
||||
attributes: { "aria-hidden": true }
|
||||
};
|
||||
|
||||
if (params.title) { properties.attributes.title = params.title; }
|
||||
|
||||
if (params.label) {
|
||||
return h('i', properties, h('span.sr-only', I18n.t(params.label)));
|
||||
} else {
|
||||
return h('i', properties);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ember.Handlebars.helper('fa-icon-bound', function(value, options) {
|
||||
return new Handlebars.SafeString(iconHTML(value, options));
|
||||
});
|
||||
@ -26,5 +45,3 @@ Ember.Handlebars.helper('fa-icon-bound', function(value, options) {
|
||||
registerUnbound('fa-icon', function(icon, params) {
|
||||
return new Handlebars.SafeString(iconHTML(icon, params));
|
||||
});
|
||||
|
||||
export { iconHTML };
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
|
||||
registerUnbound('link-domain', function(link) {
|
||||
if (link) {
|
||||
const hasTitle = (!Ember.isEmpty(Em.get(link, 'title')));
|
||||
|
||||
if (hasTitle) {
|
||||
let domain = Ember.get(link, 'domain');
|
||||
if (!Ember.isEmpty(domain)) {
|
||||
const s = domain.split('.');
|
||||
domain = s[s.length-2] + "." + s[s.length-1];
|
||||
return new Handlebars.SafeString("<span class='domain'>" + domain + "</span>");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
31
app/assets/javascripts/discourse/helpers/node.js.es6
Normal file
31
app/assets/javascripts/discourse/helpers/node.js.es6
Normal file
@ -0,0 +1,31 @@
|
||||
import { h } from 'virtual-dom';
|
||||
import { relativeAge, longDate } from 'discourse/lib/formatter';
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
export function dateNode(dt) {
|
||||
if (typeof dt === "string") { dt = new Date(dt); }
|
||||
if (dt) {
|
||||
const attributes = {
|
||||
title: longDate(dt),
|
||||
'data-time': dt.getTime(),
|
||||
'data-format': 'tiny'
|
||||
};
|
||||
|
||||
return h('span.relative-date', { attributes }, relativeAge(dt));
|
||||
}
|
||||
}
|
||||
|
||||
export function numberNode(num, opts) {
|
||||
opts = opts || {};
|
||||
num = parseInt(num, 10);
|
||||
if (isNaN(num)) { num = 0; }
|
||||
|
||||
const numString = num.toString();
|
||||
const attributes = { };
|
||||
const formatted = number(num);
|
||||
if (formatted !== numString) {
|
||||
attributes.title = numString;
|
||||
}
|
||||
|
||||
return h('span.number', { className: opts.className, attributes }, formatted);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { onToolbarCreate } from 'discourse/components/d-editor';
|
||||
import { withPluginApi } from 'discourse/lib/plugin-api';
|
||||
|
||||
export default {
|
||||
name: 'enable-emoji',
|
||||
@ -7,13 +7,15 @@ export default {
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
|
||||
if (siteSettings.enable_emoji) {
|
||||
onToolbarCreate(toolbar => {
|
||||
toolbar.addButton({
|
||||
id: 'emoji',
|
||||
group: 'extras',
|
||||
icon: 'smile-o',
|
||||
action: 'emoji',
|
||||
title: 'composer.emoji'
|
||||
withPluginApi('0.1', api => {
|
||||
api.onToolbarCreate(toolbar => {
|
||||
toolbar.addButton({
|
||||
id: 'emoji',
|
||||
group: 'extras',
|
||||
icon: 'smile-o',
|
||||
action: 'emoji',
|
||||
title: 'composer.emoji'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
name: 'ensure-image-dimensions',
|
||||
after: 'mobile',
|
||||
initialize: function() {
|
||||
initialize(container) {
|
||||
if (!window) { return; }
|
||||
|
||||
// This enforces maximum dimensions of images based on site settings
|
||||
@ -11,7 +11,8 @@ export default {
|
||||
var width = Discourse.SiteSettings.max_image_width;
|
||||
var height = Discourse.SiteSettings.max_image_height;
|
||||
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
const site = container.lookup('site:main');
|
||||
if (site.mobileView) {
|
||||
width = $(window).width() - 20;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import LogsNotice from 'discourse/services/logs-notice';
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
|
||||
export default {
|
||||
name: "logs-notice",
|
||||
after: "message-bus",
|
||||
|
||||
initialize: function (container) {
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
const messageBus = container.lookup('message-bus:main');
|
||||
const keyValueStore = container.lookup('key-value-store:main');
|
||||
LogsNotice.reopenClass(Singleton, {
|
||||
createCurrent() {
|
||||
return this.create({ messageBus, keyValueStore, siteSettings});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,14 +1,19 @@
|
||||
/**
|
||||
Initializes the `Discourse.Mobile` helper object.
|
||||
**/
|
||||
import Mobile from 'discourse/lib/mobile';
|
||||
|
||||
// Initializes the `Mobile` helper object.
|
||||
export default {
|
||||
name: 'mobile',
|
||||
after: 'inject-objects',
|
||||
|
||||
initialize: function(container) {
|
||||
Discourse.Mobile.init();
|
||||
var site = container.lookup('site:main');
|
||||
site.set('mobileView', Discourse.Mobile.mobileView);
|
||||
initialize(container, app) {
|
||||
Mobile.init();
|
||||
const site = container.lookup('site:main');
|
||||
|
||||
site.set('mobileView', Mobile.mobileView);
|
||||
site.set('isMobileDevice', Mobile.isMobileDevice);
|
||||
|
||||
// This is a bit weird but you can't seem to inject into the resolver?
|
||||
app.registry.resolver.__resolver__.mobileView = Mobile.mobileView;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { cleanDOM } from 'discourse/routes/discourse';
|
||||
import PageTracker from 'discourse/lib/page-tracker';
|
||||
import { startPageTracking, onPageChange } from 'discourse/lib/page-tracker';
|
||||
|
||||
export default {
|
||||
name: "page-tracking",
|
||||
@ -25,7 +25,6 @@ export default {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.transientCache = function(key, data, count) {
|
||||
if (data === undefined) {
|
||||
return cache[key];
|
||||
@ -34,13 +33,12 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const pageTracker = PageTracker.current();
|
||||
pageTracker.start();
|
||||
startPageTracking(router);
|
||||
|
||||
// Out of the box, Discourse tries to track google analytics
|
||||
// if it is present
|
||||
if (typeof window._gaq !== 'undefined') {
|
||||
pageTracker.on('change', function(url, title) {
|
||||
onPageChange((url, title) => {
|
||||
window._gaq.push(["_set", "title", title]);
|
||||
window._gaq.push(['_trackPageview', url]);
|
||||
});
|
||||
@ -49,7 +47,7 @@ export default {
|
||||
|
||||
// Also use Universal Analytics if it is present
|
||||
if (typeof window.ga !== 'undefined') {
|
||||
pageTracker.on('change', function(url, title) {
|
||||
onPageChange((url, title) => {
|
||||
window.ga('send', 'pageview', {page: url, title: title});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { decorateCooked } from 'discourse/lib/plugin-api';
|
||||
import HighlightSyntax from 'discourse/lib/highlight-syntax';
|
||||
import Lightbox from 'discourse/lib/lightbox';
|
||||
import highlightSyntax from 'discourse/lib/highlight-syntax';
|
||||
import lightbox from 'discourse/lib/lightbox';
|
||||
import { withPluginApi } from 'discourse/lib/plugin-api';
|
||||
|
||||
export default {
|
||||
name: "post-decorations",
|
||||
initialize: function(container) {
|
||||
decorateCooked(container, HighlightSyntax);
|
||||
decorateCooked(container, Lightbox);
|
||||
initialize() {
|
||||
withPluginApi('0.1', api => {
|
||||
api.decorateCooked(highlightSyntax);
|
||||
api.decorateCooked(lightbox);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,25 +1,23 @@
|
||||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
import Session from 'discourse/models/session';
|
||||
|
||||
const ANON_TOPIC_IDS = 2,
|
||||
ANON_PROMPT_READ_TIME = 2 * 60 * 1000,
|
||||
ONE_DAY = 24 * 60 * 60 * 1000,
|
||||
PROMPT_HIDE_DURATION = ONE_DAY;
|
||||
const ANON_TOPIC_IDS = 2;
|
||||
const ANON_PROMPT_READ_TIME = 2 * 60 * 1000;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
const PROMPT_HIDE_DURATION = ONE_DAY;
|
||||
|
||||
export default {
|
||||
name: "signup-cta",
|
||||
|
||||
initialize(container) {
|
||||
const screenTrack = ScreenTrack.current(),
|
||||
session = Session.current(),
|
||||
siteSettings = container.lookup('site-settings:main'),
|
||||
keyValueStore = container.lookup('key-value-store:main'),
|
||||
user = container.lookup('current-user:main');
|
||||
const screenTrack = container.lookup('screen-track:main');
|
||||
const session = Session.current();
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
const keyValueStore = container.lookup('key-value-store:main');
|
||||
const user = container.lookup('current-user:main');
|
||||
|
||||
screenTrack.set('keyValueStore', keyValueStore);
|
||||
screenTrack.keyValueStore = keyValueStore;
|
||||
|
||||
// Preconditions
|
||||
|
||||
if (user) return; // must not be logged in
|
||||
if (keyValueStore.get('anon-cta-never')) return; // "never show again"
|
||||
if (!siteSettings.allow_new_registrations) return;
|
||||
@ -63,7 +61,7 @@ export default {
|
||||
session.set('showSignupCta', true);
|
||||
}
|
||||
|
||||
screenTrack.set('anonFlushCallback', checkSignupCtaRequirements);
|
||||
screenTrack.registerAnonCallback(checkSignupCtaRequirements);
|
||||
|
||||
checkSignupCtaRequirements();
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ export default {
|
||||
});
|
||||
|
||||
if (!Ember.testing) {
|
||||
if (!Discourse.Mobile.mobileView) {
|
||||
if (!site.mobileView) {
|
||||
bus.subscribe("/notification-alert/" + user.get('id'), function(data){
|
||||
onNotification(data, user);
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
@module $.fn.autocomplete
|
||||
**/
|
||||
|
||||
export var CANCELLED_STATUS = "__CANCELLED";
|
||||
|
||||
const allowedLettersRegex = /[\s\t\[\{\(\/]/;
|
||||
@ -45,7 +44,7 @@ export default function(options) {
|
||||
if (options === 'destroy') {
|
||||
Ember.run.cancel(inputTimeout);
|
||||
|
||||
$(this).off('keypress.autocomplete')
|
||||
$(this).off('keyup.autocomplete')
|
||||
.off('keydown.autocomplete')
|
||||
.off('paste.autocomplete')
|
||||
.off('click.autocomplete');
|
||||
@ -59,7 +58,13 @@ export default function(options) {
|
||||
}
|
||||
|
||||
if (this.length !== 1) {
|
||||
alert("only supporting one matcher at the moment");
|
||||
if (window.console) {
|
||||
window.console.log("WARNING: passed multiple elements to $.autocomplete, skipping.");
|
||||
if (window.Error) {
|
||||
window.console.log((new window.Error()).stack);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
var disabled = options && options.disabled;
|
||||
@ -70,6 +75,7 @@ export default function(options) {
|
||||
var completeEnd = null;
|
||||
var me = this;
|
||||
var div = null;
|
||||
var prevTerm = null;
|
||||
|
||||
// input is handled differently
|
||||
var isInput = this[0].tagName === "INPUT";
|
||||
@ -82,6 +88,7 @@ export default function(options) {
|
||||
div = null;
|
||||
completeStart = null;
|
||||
autocompleteOptions = null;
|
||||
prevTerm = null;
|
||||
};
|
||||
|
||||
var addInputSelectedItem = function(item) {
|
||||
@ -139,6 +146,10 @@ export default function(options) {
|
||||
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 (options && options.afterComplete) {
|
||||
options.afterComplete(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -226,7 +237,7 @@ export default function(options) {
|
||||
vOffset = div.height();
|
||||
}
|
||||
|
||||
if (Discourse.Mobile.mobileView && !isInput) {
|
||||
if (Discourse.Site.currentProp('mobileView') && !isInput) {
|
||||
div.css('width', 'auto');
|
||||
|
||||
if ((me.height() / 2) >= pos.top) { vOffset = -23; }
|
||||
@ -242,7 +253,14 @@ export default function(options) {
|
||||
});
|
||||
};
|
||||
|
||||
const SKIP = "skip";
|
||||
|
||||
const dataSource = (term, opts) => {
|
||||
if (prevTerm === term) {
|
||||
return SKIP;
|
||||
}
|
||||
|
||||
prevTerm = term;
|
||||
if (term.length !== 0 && term.trim().length === 0) {
|
||||
closeAutocomplete();
|
||||
return null;
|
||||
@ -251,9 +269,9 @@ export default function(options) {
|
||||
}
|
||||
};
|
||||
|
||||
var updateAutoComplete = function(r) {
|
||||
const updateAutoComplete = function(r) {
|
||||
|
||||
if (completeStart === null) return;
|
||||
if (completeStart === null || r === SKIP) return;
|
||||
|
||||
if (r && r.then && typeof(r.then) === "function") {
|
||||
if (div) {
|
||||
@ -304,21 +322,21 @@ export default function(options) {
|
||||
}
|
||||
};
|
||||
|
||||
$(this).on('keypress.autocomplete', function(e) {
|
||||
var caretPosition, term;
|
||||
$(this).on('keyup.autocomplete', function() {
|
||||
|
||||
// keep hunting backwards till you hit a the @ key
|
||||
if (options.key && e.which === options.key.charCodeAt(0)) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
var prevChar = me.val().charAt(caretPosition - 1);
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
completeStart = completeEnd = caretPosition;
|
||||
updateAutoComplete(dataSource("", options));
|
||||
var caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
|
||||
if (options.key && completeStart === null && caretPosition > 0) {
|
||||
var key = me[0].value[caretPosition-1];
|
||||
if (key === options.key) {
|
||||
var prevChar = me.val().charAt(caretPosition-2);
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
completeStart = completeEnd = caretPosition-1;
|
||||
updateAutoComplete(dataSource("", options));
|
||||
}
|
||||
}
|
||||
} else if ((completeStart !== null) && (e.charCode !== 0)) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
term += String.fromCharCode(e.charCode);
|
||||
} else if (completeStart !== null) {
|
||||
var term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
}
|
||||
});
|
||||
@ -331,7 +349,7 @@ export default function(options) {
|
||||
}
|
||||
|
||||
if(options.allowAny){
|
||||
// saves us wiring up a change event as well, keypress is while its pressed
|
||||
// saves us wiring up a change event as well
|
||||
|
||||
Ember.run.cancel(inputTimeout);
|
||||
inputTimeout = Ember.run.later(function(){
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import PageTracker from 'discourse/lib/page-tracker';
|
||||
import KeyValueStore from 'discourse/lib/key-value-store';
|
||||
import { onPageChange } from 'discourse/lib/page-tracker';
|
||||
|
||||
let primaryTab = false;
|
||||
let liveEnabled = false;
|
||||
@ -84,7 +84,8 @@ function setupNotifications() {
|
||||
if (document) {
|
||||
document.addEventListener("scroll", resetIdle);
|
||||
}
|
||||
PageTracker.on("change", resetIdle);
|
||||
|
||||
onPageChange(resetIdle);
|
||||
}
|
||||
|
||||
function resetIdle() {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import CloakedCollectionView from 'discourse/views/cloaked-collection';
|
||||
|
||||
/**
|
||||
@module Discourse
|
||||
*/
|
||||
@ -33,7 +31,12 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@method initState
|
||||
*/
|
||||
initState() {
|
||||
set(this, 'history', get(this, 'history') || window.history);
|
||||
const history = get(this, 'history') || window.history;
|
||||
if (history && history.scrollRestoration) {
|
||||
history.scrollRestoration = "manual";
|
||||
}
|
||||
|
||||
set(this, 'history', history);
|
||||
|
||||
let url = this.formatURL(this.getURL());
|
||||
const loc = get(this, 'location');
|
||||
@ -221,36 +224,4 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
Since we're using pushState/replaceState let's add extra hooks to cloakedView to
|
||||
eject itself when the popState occurs. This results in better back button
|
||||
behavior.
|
||||
**/
|
||||
CloakedCollectionView.reopen({
|
||||
_watchForPopState: function() {
|
||||
const self = this,
|
||||
cb = function() {
|
||||
// Sam: This is a hack, but a very important one
|
||||
// Due to the way we use replace state the back button works strangely
|
||||
//
|
||||
// If you visit a topic from the front page, scroll a bit around and then hit back
|
||||
// you notice that first the page scrolls a bit (within the topic) and then it goes back
|
||||
// this transition is jarring and adds uneeded rendering costs.
|
||||
//
|
||||
// To repro comment the hack out and wack a debugger statement here and in
|
||||
// topic_route deactivate
|
||||
$('.posts,#topic-title').hide();
|
||||
self.cleanUp();
|
||||
self.set('controller.model.postStream.loaded', false);
|
||||
};
|
||||
this.set('_callback', cb);
|
||||
popstateCallbacks.addObject(cb);
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_disbandWatcher: function() {
|
||||
popstateCallbacks.removeObject(this.get('_callback'));
|
||||
this.set('_callback', null);
|
||||
}.on('willDestroyElement')
|
||||
});
|
||||
|
||||
export default DiscourseLocation;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -161,7 +161,7 @@ function showSelector(options) {
|
||||
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
|
||||
$('.emoji-modal-wrapper').click(() => closeSelector());
|
||||
|
||||
if (Discourse.Mobile.mobileView) PER_ROW = 9;
|
||||
if (Discourse.Site.currentProp('mobileView')) { PER_ROW = 9; }
|
||||
const page = keyValueStore.getInt("emojiPage", 0);
|
||||
const offset = keyValueStore.getInt("emojiOffset", 0);
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// TODO @robin to move this whole thing to es6
|
||||
Discourse.Emoji = {};
|
||||
|
||||
// bump up this number to expire all emojis
|
||||
Discourse.Emoji.ImageVersion = "1"
|
||||
Discourse.Emoji.ImageVersion = "2"
|
||||
|
||||
var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
var aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
|
||||
@ -12,6 +13,15 @@ Discourse.Dialect.registerEmoji = function(code, url) {
|
||||
extendedEmoji[code] = url;
|
||||
};
|
||||
|
||||
var _unicodeReplacements;
|
||||
var _unicodeRegexp;
|
||||
Discourse.Dialect.setUnicodeReplacements = function(replacements) {
|
||||
_unicodeReplacements = replacements;
|
||||
if (replacements) {
|
||||
_unicodeRegexp = new RegExp(Object.keys(replacements).join("|"), "g");
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used by PrettyText to reset custom emojis in multisites
|
||||
Discourse.Dialect.resetEmojis = function() {
|
||||
extendedEmoji = {};
|
||||
@ -38,9 +48,11 @@ var emojiHash = {};
|
||||
// add all default emojis
|
||||
emoji.forEach(function(code){ emojiHash[code] = true; });
|
||||
// and their aliases
|
||||
|
||||
var aliasHash = {};
|
||||
for (var name in aliases) {
|
||||
aliases[name].forEach(function(alias) {
|
||||
emojiHash[alias] = true;
|
||||
aliasHash[alias] = name;
|
||||
});
|
||||
}
|
||||
|
||||
@ -99,8 +111,8 @@ function imageFor(code) {
|
||||
|
||||
// Also support default emotions
|
||||
var translations = {
|
||||
':)' : 'slightly_smiling',
|
||||
':-)' : 'slightly_smiling',
|
||||
':)' : 'slight_smile',
|
||||
':-)' : 'slight_smile',
|
||||
':(' : 'frowning',
|
||||
':-(' : 'frowning',
|
||||
';)' : 'wink',
|
||||
@ -151,6 +163,19 @@ Object.keys(translations).forEach(function (t) {
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.Dialect.addPreProcessor(function(text) {
|
||||
if (_unicodeReplacements) {
|
||||
_unicodeRegexp.lastIndex = 0;
|
||||
|
||||
var m;
|
||||
while ((m = _unicodeRegexp.exec(text)) !== null) {
|
||||
text = text.replace(m[0], ":" + _unicodeReplacements[m[0]] + ":");
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
|
||||
}
|
||||
@ -198,27 +223,29 @@ Discourse.Emoji.search = function(term, options) {
|
||||
var maxResults = (options && options["maxResults"]) || -1;
|
||||
if (maxResults === 0) { return []; }
|
||||
|
||||
toSearch = toSearch || _.union(_.keys(emojiHash), _.keys(extendedEmoji)).sort();
|
||||
toSearch = toSearch || _.union(_.keys(emojiHash), _.keys(extendedEmoji), _.keys(aliasHash)).sort();
|
||||
|
||||
var i, results = [];
|
||||
|
||||
var done = function() {
|
||||
function addResult(term) {
|
||||
var val = aliasHash[term] || term;
|
||||
if (results.indexOf(val) === -1) {
|
||||
results.push(val);
|
||||
}
|
||||
return maxResults > 0 && results.length >= maxResults;
|
||||
}
|
||||
|
||||
for (i=0; i < toSearch.length; i++) {
|
||||
if (toSearch[i].indexOf(term) === 0) {
|
||||
results.push(toSearch[i]);
|
||||
if(done()) { break; }
|
||||
var item;
|
||||
for (i=0; i<toSearch.length; i++) {
|
||||
item = toSearch[i];
|
||||
if (item.indexOf(term) === 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
if(!done()){
|
||||
for (i=0; i < toSearch.length; i++) {
|
||||
if (toSearch[i].indexOf(term) > 0) {
|
||||
results.push(toSearch[i]);
|
||||
if(done()) { break; }
|
||||
}
|
||||
for (i=0; i<toSearch.length; i++) {
|
||||
item = toSearch[i];
|
||||
if (item.indexOf(term) === 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ const bindings = {
|
||||
'home': {handler: 'goToFirstPost', anonymous: true},
|
||||
'j': {handler: 'selectDown', anonymous: true},
|
||||
'k': {handler: 'selectUp', anonymous: true},
|
||||
'l': {click: '.topic-post.selected button[data-action="like"]'},
|
||||
'l': {click: '.topic-post.selected button.toggle-like'},
|
||||
'm m': {click: 'div.notification-options li[data-id="0"] a'}, // mark topic as muted
|
||||
'm r': {click: 'div.notification-options li[data-id="1"] a'}, // mark topic as regular
|
||||
'm t': {click: 'div.notification-options li[data-id="2"] a'}, // mark topic as tracking
|
||||
@ -222,10 +222,14 @@ export default {
|
||||
// TODO: We should keep track of the post without a CSS class
|
||||
const selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10);
|
||||
if (selectedPostId) {
|
||||
const topicController = container.lookup('controller:topic'),
|
||||
post = topicController.get('model.postStream.posts').findBy('id', selectedPostId);
|
||||
const topicController = container.lookup('controller:topic');
|
||||
const post = topicController.get('model.postStream.posts').findBy('id', selectedPostId);
|
||||
if (post) {
|
||||
topicController.send(action, post);
|
||||
// TODO: Use ember closure actions
|
||||
const result = topicController._actions[action].call(topicController, post);
|
||||
if (result && result.then) {
|
||||
this.appEvents.trigger('post-stream:refresh', { id: selectedPostId });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -287,7 +291,7 @@ export default {
|
||||
index = 0;
|
||||
$articles.each(function() {
|
||||
const top = $(this).position().top;
|
||||
if (top > scrollTop) {
|
||||
if (top >= scrollTop) {
|
||||
return false;
|
||||
}
|
||||
index += 1;
|
||||
@ -312,12 +316,7 @@ export default {
|
||||
}
|
||||
|
||||
if ($article.is('.topic-post')) {
|
||||
let tabLoc = $article.find('a.tabLoc');
|
||||
if (tabLoc.length === 0) {
|
||||
tabLoc = $('<a href class="tabLoc"></a>');
|
||||
$article.prepend(tabLoc);
|
||||
}
|
||||
tabLoc.focus();
|
||||
$('a.tabLoc', $article).focus();
|
||||
}
|
||||
|
||||
this._scrollList($article, direction);
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
// An object that is responsible for logic related to mobile devices.
|
||||
Discourse.Mobile = {
|
||||
const Mobile = {
|
||||
isMobileDevice: false,
|
||||
mobileView: false,
|
||||
|
||||
init: function() {
|
||||
var $html = $('html');
|
||||
init() {
|
||||
const $html = $('html');
|
||||
this.isMobileDevice = $html.hasClass('mobile-device');
|
||||
this.mobileView = $html.hasClass('mobile-view');
|
||||
|
||||
@ -42,3 +42,13 @@ Discourse.Mobile = {
|
||||
window.location.assign(window.location.pathname + '?mobile_view=' + (mobile ? '1' : '0'));
|
||||
}
|
||||
};
|
||||
|
||||
// Backwards compatibiltity, deprecated
|
||||
Object.defineProperty(Discourse, 'Mobile', {
|
||||
get: function() {
|
||||
Ember.warn("DEPRECATION: `Discourse.Mobile` is deprecated, use `this.site.mobileView` instead");
|
||||
return Mobile;
|
||||
}
|
||||
});
|
||||
|
||||
export default Mobile;
|
||||
@ -1,37 +1,34 @@
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
const PageTracker = Ember.Object.extend(Ember.Evented);
|
||||
let _pageTracker = PageTracker.create();
|
||||
|
||||
/**
|
||||
Called whenever the "page" changes. This allows us to set up analytics
|
||||
and other tracking.
|
||||
let _started = false;
|
||||
export function startPageTracking(router) {
|
||||
if (_started) { return; }
|
||||
|
||||
To get notified when the page changes, you can install a hook like so:
|
||||
router.on('didTransition', function() {
|
||||
this.send('refreshTitle');
|
||||
const url = Discourse.getURL(this.get('url'));
|
||||
|
||||
```javascript
|
||||
PageTracker.current().on('change', function(url, title) {
|
||||
console.log('the page changed to: ' + url + ' and title ' + title);
|
||||
// Refreshing the title is debounced, so we need to trigger this in the
|
||||
// next runloop to have the correct title.
|
||||
Em.run.next(() => {
|
||||
_pageTracker.trigger('change', url, Discourse.get('_docTitle'));
|
||||
});
|
||||
```
|
||||
**/
|
||||
const PageTracker = Ember.Object.extend(Ember.Evented, {
|
||||
start: function() {
|
||||
if (this.get('started')) { return; }
|
||||
});
|
||||
_started = true;
|
||||
}
|
||||
|
||||
var router = Discourse.__container__.lookup('router:main'),
|
||||
self = this;
|
||||
export function onPageChange(fn) {
|
||||
_pageTracker.on('change', fn);
|
||||
}
|
||||
|
||||
router.on('didTransition', function() {
|
||||
this.send('refreshTitle');
|
||||
var url = Discourse.getURL(this.get('url'));
|
||||
|
||||
// Refreshing the title is debounced, so we need to trigger this in the
|
||||
// next runloop to have the correct title.
|
||||
Em.run.next(function() {
|
||||
self.trigger('change', url, Discourse.get('_docTitle'));
|
||||
});
|
||||
});
|
||||
this.set('started', true);
|
||||
// backwards compatibility
|
||||
const BackwardsCompat = {
|
||||
current() {
|
||||
console.warn(`Using PageTracker.current() is deprecated. Your plugin should use the PluginAPI`);
|
||||
return _pageTracker;
|
||||
}
|
||||
});
|
||||
PageTracker.reopenClass(Singleton);
|
||||
};
|
||||
|
||||
export default PageTracker;
|
||||
Discourse.PageTracker = BackwardsCompat;
|
||||
export default BackwardsCompat;
|
||||
|
||||
@ -1,4 +1,302 @@
|
||||
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||
import { addDecorator } from 'discourse/widgets/post-cooked';
|
||||
import ComposerEditor from 'discourse/components/composer-editor';
|
||||
import { addButton } from 'discourse/widgets/post-menu';
|
||||
import { includeAttributes } from 'discourse/lib/transform-post';
|
||||
import { addToolbarCallback } from 'discourse/components/d-editor';
|
||||
import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
|
||||
import { decorateWidget, changeSetting } from 'discourse/widgets/widget';
|
||||
import { onPageChange } from 'discourse/lib/page-tracker';
|
||||
import { preventCloak } from 'discourse/widgets/post-stream';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
this.version = version;
|
||||
this.container = container;
|
||||
this._currentUser = container.lookup('current-user:main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to retrieve the currently logged in user within your plugin.
|
||||
* If the user is not logged in, it will be `null`.
|
||||
**/
|
||||
getCurrentUser() {
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for decorating the `cooked` content of a post after it is rendered using
|
||||
* jQuery.
|
||||
*
|
||||
* `callback` will be called when it is time to decorate with a jQuery selector.
|
||||
*
|
||||
* Use `options.onlyStream` if you only want to decorate posts within a topic,
|
||||
* and not in other places like the user stream.
|
||||
*
|
||||
* For example, to add a yellow background to all posts you could do this:
|
||||
*
|
||||
* ```
|
||||
* api.decorateCooked($elem => $elem.css({ backgroundColor: 'yellow' }));
|
||||
* ```
|
||||
**/
|
||||
decorateCooked(callback, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
addDecorator(callback);
|
||||
|
||||
if (!opts.onlyStream) {
|
||||
decorate(ComposerEditor, 'previewRefreshed', callback);
|
||||
decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* addPosterIcon(callback)
|
||||
*
|
||||
* This function can be used to add an icon with a link that will be displayed
|
||||
* beside a poster's name. The `callback` is called with the post's user custom
|
||||
* fields and post attrions. An icon will be rendered if the callback returns
|
||||
* an object with the appropriate attributes.
|
||||
*
|
||||
* The returned object can have the following attributes:
|
||||
*
|
||||
* icon the font awesome icon to render
|
||||
* emoji an emoji icon to render
|
||||
* className (optional) a css class to apply to the icon
|
||||
* url (optional) where to link the icon
|
||||
* title (optional) the tooltip title for the icon on hover
|
||||
*
|
||||
* ```
|
||||
* api.addPosterIcon((cfs, attrs) => {
|
||||
* if (cfs.customer) {
|
||||
* return { icon: 'user', className: 'customer', title: 'customer' };
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
**/
|
||||
addPosterIcon(cb) {
|
||||
decorateWidget('poster-name:after', dec => {
|
||||
const attrs = dec.attrs;
|
||||
|
||||
const result = cb(attrs.userCustomFields || {}, attrs);
|
||||
if (result) {
|
||||
let iconBody;
|
||||
|
||||
if (result.icon) {
|
||||
iconBody = iconNode(result.icon);
|
||||
} else if (result.emoji) {
|
||||
iconBody = result.emoji.split('|').map(emoji => {
|
||||
const src = Discourse.Emoji.urlFor(emoji);
|
||||
return dec.h('img', { className: 'emoji', attributes: { src } });
|
||||
});
|
||||
}
|
||||
|
||||
if (result.text) {
|
||||
iconBody = [iconBody, result.text];
|
||||
}
|
||||
|
||||
if (result.url) {
|
||||
iconBody = dec.h('a', { attributes: { href: result.url } }, iconBody);
|
||||
}
|
||||
|
||||
|
||||
return dec.h('span',
|
||||
{ className: result.className, attributes: { title: result.title } },
|
||||
iconBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The main interface for extending widgets with additional HTML.
|
||||
*
|
||||
* The `name` you pass it should be the name of the widget and a type
|
||||
* for the decorator. All widgets support `before` and `after` types.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* api.decorateWidget('post:after', () => {
|
||||
* return "I am displayed after every post!";
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Your decorator will be called with an instance of a `DecoratorHelper`
|
||||
* object, which provides methods you can use to build more interesting
|
||||
* formatting.
|
||||
*
|
||||
* ```
|
||||
* api.decorateWidget('post:after', helper => {
|
||||
* return helper.h('p.fancy', `I'm an HTML paragraph on post with id ${helper.attrs.id}`);
|
||||
* });
|
||||
*
|
||||
* (View the source for `DecoratorHelper` for more helper methods you
|
||||
* can use in your plugin decorators.)
|
||||
*
|
||||
**/
|
||||
decorateWidget(name, fn) {
|
||||
decorateWidget(name, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to a widget that already exists. You can use this to
|
||||
* add additional functionality from your plugin.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* api.attachWidgetAction('post', 'annoyMe', () => {
|
||||
* alert('ANNOYED!');
|
||||
* });
|
||||
* ```
|
||||
**/
|
||||
attachWidgetAction(widget, actionName, fn) {
|
||||
const widgetClass = this.container.lookupFactory(`widget:${widget}`);
|
||||
widgetClass.prototype[actionName] = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add more attributes to the Post's `attrs` object passed through to widgets.
|
||||
* You'll need to do this if you've added attributes to the serializer for a
|
||||
* Post and want to use them when you're rendering.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // attrs.poster_age and attrs.poster_height will be present
|
||||
* api.includePostAttributes('poster_age', 'poster_height');
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
includePostAttributes(...attributes) {
|
||||
includeAttributes(...attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new button below a post with your plugin.
|
||||
*
|
||||
* The `callback` function will be called whenever the post menu is rendered,
|
||||
* and if you return an object with the button details it will be rendered.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* api.addPostMenuButton('coffee', () => {
|
||||
* return {
|
||||
* action: 'drinkCoffee',
|
||||
* icon: 'coffee',
|
||||
* className: 'hot-coffee',
|
||||
* title: 'coffee.title',
|
||||
* position: 'first' // can be `first`, `last` or `second-last-hidden`
|
||||
* };
|
||||
* });
|
||||
**/
|
||||
addPostMenuButton(name, callback) {
|
||||
addButton(name, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that is called when the editor toolbar is created. You can
|
||||
* use this to add custom editor buttons.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* api.onToolbarCreate(toolbar => {
|
||||
* toolbar.addButton({
|
||||
* id: 'pop-text',
|
||||
* group: 'extras',
|
||||
* icon: 'bolt',
|
||||
* action: 'makeItPop',
|
||||
* title: 'pop_format.title'
|
||||
* });
|
||||
* });
|
||||
**/
|
||||
onToolbarCreate(callback) {
|
||||
addToolbarCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that is called when the post stream is removed from the DOM.
|
||||
* This advanced hook should be used if you end up wiring up any
|
||||
* events that need to be torn down when the user leaves the topic
|
||||
* page.
|
||||
**/
|
||||
cleanupStream(fn) {
|
||||
addWidgetCleanCallback('post-stream', fn);
|
||||
}
|
||||
|
||||
/**
|
||||
Called whenever the "page" changes. This allows us to set up analytics
|
||||
and other tracking.
|
||||
|
||||
To get notified when the page changes, you can install a hook like so:
|
||||
|
||||
```javascript
|
||||
api.onPageChange((url, title) => {
|
||||
console.log('the page changed to: ' + url + ' and title ' + title);
|
||||
});
|
||||
```
|
||||
**/
|
||||
onPageChange(fn) {
|
||||
onPageChange(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a setting associated with a widget. For example, if
|
||||
* you wanted small avatars in the post stream:
|
||||
*
|
||||
* ```javascript
|
||||
* api.changeWidgetSetting('post-avatar', 'size', 'small');
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
changeWidgetSetting(widgetName, settingName, newValue) {
|
||||
changeSetting(widgetName, settingName, newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents an element in the post stream from being cloaked.
|
||||
* This is useful if you are using a plugin such as youtube
|
||||
* and don't want the video removed once it has begun
|
||||
* playing.
|
||||
*
|
||||
* ```javascript
|
||||
* api.preventCloak(1234);
|
||||
* ```
|
||||
**/
|
||||
preventCloak(postId) {
|
||||
preventCloak(postId);
|
||||
}
|
||||
}
|
||||
|
||||
let _pluginv01;
|
||||
function getPluginApi(version) {
|
||||
if (version === "0.1") {
|
||||
if (!_pluginv01) {
|
||||
_pluginv01 = new PluginApi(version, Discourse.__container__);
|
||||
}
|
||||
return _pluginv01;
|
||||
} else {
|
||||
console.warn(`Plugin API v${version} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* withPluginApi(version, apiCode, noApi)
|
||||
*
|
||||
* Helper to version our client side plugin API. Pass the version of the API that your
|
||||
* plugin is coded against. If that API is available, the `apiCodeCallback` function will
|
||||
* be called with the `PluginApi` object.
|
||||
*/
|
||||
export function withPluginApi(version, apiCodeCallback, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const api = getPluginApi(version);
|
||||
if (api) {
|
||||
return apiCodeCallback(api);
|
||||
}
|
||||
}
|
||||
|
||||
let _decorateId = 0;
|
||||
function decorate(klass, evt, cb) {
|
||||
@ -7,38 +305,6 @@ function decorate(klass, evt, cb) {
|
||||
klass.reopen(mixin);
|
||||
}
|
||||
|
||||
export function decorateCooked(container, cb) {
|
||||
const postView = container.lookupFactory('view:post');
|
||||
decorate(postView, 'postViewInserted', cb);
|
||||
decorate(postView, 'postViewUpdated', cb);
|
||||
decorate(ComposerEditor, 'previewRefreshed', cb);
|
||||
decorate(container.lookupFactory('view:embedded-post'), 'didInsertElement', cb);
|
||||
decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb);
|
||||
}
|
||||
|
||||
// This is backported so plugins in the new format will not raise errors
|
||||
//
|
||||
// To upgrade your plugin for backwards compatibility, you can add code in this
|
||||
// form:
|
||||
//
|
||||
// function newApiCode(api) {
|
||||
// // api.xyz();
|
||||
// }
|
||||
//
|
||||
// function oldCode() {
|
||||
// // your pre-PluginAPI code goes here. You will be able to delete this
|
||||
// // code once the `PluginAPI` has been rolled out to all versions of
|
||||
// // Discourse you want to support.
|
||||
// }
|
||||
//
|
||||
// // `newApiCode` will use API version 0.1, if no API support then
|
||||
// // `oldCode` will be called
|
||||
// withPluginApi('0.1', newApiCode, { noApi: oldCode });
|
||||
//
|
||||
export function withPluginApi(version, apiCodeCallback, opts) {
|
||||
console.warn(`Plugin API v${version} is not supported`);
|
||||
|
||||
if (opts && opts.noApi) {
|
||||
return opts.noApi();
|
||||
}
|
||||
export function decorateCooked() {
|
||||
console.warn('`decorateCooked` has been removed. Use `getPluginApi(version).decorateCooked` instead');
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Placeholder } from 'discourse/views/cloaked';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export function Placeholder(viewName) {
|
||||
this.viewName = viewName;
|
||||
}
|
||||
|
||||
export default Ember.Object.extend(Ember.Array, {
|
||||
posts: null,
|
||||
@ -34,6 +36,11 @@ export default Ember.Object.extend(Ember.Array, {
|
||||
this._changeArray(cb, this.get('posts.length') - 1, 1, 0);
|
||||
},
|
||||
|
||||
refreshAll(cb) {
|
||||
const length = this.get('posts.length');
|
||||
this._changeArray(cb, 0, length, length);
|
||||
},
|
||||
|
||||
appending(postIds) {
|
||||
this._changeArray(() => {
|
||||
const appendingIds = this._appendingIds;
|
||||
|
||||
@ -37,8 +37,6 @@ function positioningWorkaround($fixedElement) {
|
||||
if (evt) {
|
||||
evt.target.removeEventListener('blur', blurred);
|
||||
}
|
||||
|
||||
$('body').removeData('disable-cloaked-view');
|
||||
};
|
||||
|
||||
var blurred = _.debounce(blurredNow, 250);
|
||||
@ -63,7 +61,6 @@ function positioningWorkaround($fixedElement) {
|
||||
|
||||
// take care of body
|
||||
|
||||
$('body').data('disable-cloaked-view',true);
|
||||
$('#main-outlet').hide();
|
||||
$('header').hide();
|
||||
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
// We use this class to track how long posts in a topic are on the screen.
|
||||
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3;
|
||||
const MAX_TRACKING_TIME = 1000 * 60 * 6;
|
||||
const ANON_MAX_TOPIC_IDS = 5;
|
||||
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
const getTime = () => new Date().getTime();
|
||||
|
||||
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3,
|
||||
MAX_TRACKING_TIME = 1000 * 60 * 6,
|
||||
ANON_MAX_TOPIC_IDS = 5;
|
||||
|
||||
const ScreenTrack = Ember.Object.extend({
|
||||
|
||||
init() {
|
||||
export default class {
|
||||
constructor(topicTrackingState, siteSettings, session, currentUser) {
|
||||
this.topicTrackingState = topicTrackingState;
|
||||
this.siteSettings = siteSettings;
|
||||
this.session = session;
|
||||
this.currentUser = currentUser;
|
||||
this.reset();
|
||||
|
||||
// TODO: Move `ScreenTrack` to injection and remove this
|
||||
this.set('topicTrackingState', Discourse.__container__.lookup('topic-tracking-state:main'));
|
||||
},
|
||||
}
|
||||
|
||||
start(topicId, topicController) {
|
||||
const currentTopicId = this.get('topicId');
|
||||
const currentTopicId = this._topicId;
|
||||
if (currentTopicId && (currentTopicId !== topicId)) {
|
||||
this.tick();
|
||||
this.flush();
|
||||
@ -25,90 +24,81 @@ const ScreenTrack = Ember.Object.extend({
|
||||
this.reset();
|
||||
|
||||
// Create an interval timer if we don't have one.
|
||||
if (!this.get('interval')) {
|
||||
const self = this;
|
||||
this.set('interval', setInterval(function () {
|
||||
self.tick();
|
||||
}, 1000));
|
||||
|
||||
$(window).on('scroll.screentrack', function(){self.scrolled();});
|
||||
if (!this._interval) {
|
||||
this._interval = setInterval(() => this.tick(), 1000);
|
||||
$(window).on('scroll.screentrack', () => this.scrolled());
|
||||
}
|
||||
|
||||
this.set('topicId', topicId);
|
||||
this.set('topicController', topicController);
|
||||
},
|
||||
this._topicId = topicId;
|
||||
this._topicController = topicController;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if(!this.get('topicId')) {
|
||||
// already stopped no need to "extra stop"
|
||||
return;
|
||||
}
|
||||
// already stopped no need to "extra stop"
|
||||
if(!this._topicId) { return; }
|
||||
|
||||
$(window).off('scroll.screentrack');
|
||||
this.tick();
|
||||
this.flush();
|
||||
this.reset();
|
||||
this.set('topicId', null);
|
||||
this.set('topicController', null);
|
||||
if (this.get('interval')) {
|
||||
clearInterval(this.get('interval'));
|
||||
this.set('interval', null);
|
||||
|
||||
this._topicId = null;
|
||||
this._topicController = null;
|
||||
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
track(elementId, postNumber) {
|
||||
this.get('timings')["#" + elementId] = {
|
||||
time: 0,
|
||||
postNumber: postNumber
|
||||
};
|
||||
},
|
||||
|
||||
stopTracking(elementId) {
|
||||
delete this.get('timings')['#' + elementId];
|
||||
},
|
||||
setOnscreen(onscreen) {
|
||||
this._onscreen = onscreen;
|
||||
}
|
||||
|
||||
// Reset our timers
|
||||
reset() {
|
||||
this.setProperties({
|
||||
lastTick: new Date().getTime(),
|
||||
lastScrolled: new Date().getTime(),
|
||||
lastFlush: 0,
|
||||
cancelled: false,
|
||||
timings: {},
|
||||
totalTimings: {},
|
||||
topicTime: 0
|
||||
});
|
||||
},
|
||||
const now = getTime();
|
||||
this._lastTick = now;
|
||||
this._lastScrolled = now;
|
||||
this._lastFlush = 0;
|
||||
this._timings = {};
|
||||
this._totalTimings = {};
|
||||
this._topicTime = 0;
|
||||
this._onscreen = [];
|
||||
}
|
||||
|
||||
scrolled() {
|
||||
this.set('lastScrolled', new Date().getTime());
|
||||
},
|
||||
this._lastScrolled = getTime();
|
||||
}
|
||||
|
||||
registerAnonCallback(cb) {
|
||||
this._anonCallback = cb;
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.get('cancelled')) { return; }
|
||||
const newTimings = {};
|
||||
const totalTimings = this._totalTimings;
|
||||
|
||||
const newTimings = {},
|
||||
totalTimings = this.get('totalTimings'),
|
||||
self = this;
|
||||
const timings = this._timings;
|
||||
Object.keys(this._timings).forEach(postNumber => {
|
||||
const time = timings[postNumber];
|
||||
totalTimings[postNumber] = totalTimings[postNumber] || 0;
|
||||
|
||||
_.each(this.get('timings'), function(timing) {
|
||||
if (!totalTimings[timing.postNumber])
|
||||
totalTimings[timing.postNumber] = 0;
|
||||
|
||||
if (timing.time > 0 && totalTimings[timing.postNumber] < MAX_TRACKING_TIME) {
|
||||
totalTimings[timing.postNumber] += timing.time;
|
||||
newTimings[timing.postNumber] = timing.time;
|
||||
if (time > 0 && totalTimings[postNumber] < MAX_TRACKING_TIME) {
|
||||
totalTimings[postNumber] += time;
|
||||
newTimings[postNumber] = time;
|
||||
}
|
||||
timing.time = 0;
|
||||
timings[postNumber] = 0;
|
||||
});
|
||||
|
||||
const topicId = parseInt(this.get('topicId'), 10);
|
||||
const topicId = parseInt(this._topicId, 10);
|
||||
let highestSeen = 0;
|
||||
|
||||
_.each(newTimings, function(time,postNumber) {
|
||||
Object.keys(newTimings).forEach(postNumber => {
|
||||
highestSeen = Math.max(highestSeen, parseInt(postNumber, 10));
|
||||
});
|
||||
|
||||
const highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic');
|
||||
const highestSeenByTopic = this.session.get('highestSeenByTopic');
|
||||
if ((highestSeenByTopic[topicId] || 0) < highestSeen) {
|
||||
highestSeenByTopic[topicId] = highestSeen;
|
||||
}
|
||||
@ -116,11 +106,11 @@ const ScreenTrack = Ember.Object.extend({
|
||||
this.topicTrackingState.updateSeen(topicId, highestSeen);
|
||||
|
||||
if (!$.isEmptyObject(newTimings)) {
|
||||
if (Discourse.User.current()) {
|
||||
if (this.currentUser) {
|
||||
Discourse.ajax('/topics/timings', {
|
||||
data: {
|
||||
timings: newTimings,
|
||||
topic_time: this.get('topicTime'),
|
||||
topic_time: this._topicTime,
|
||||
topic_id: topicId
|
||||
},
|
||||
cache: false,
|
||||
@ -128,22 +118,20 @@ const ScreenTrack = Ember.Object.extend({
|
||||
headers: {
|
||||
'X-SILENCE-LOGGER': 'true'
|
||||
}
|
||||
}).then(function() {
|
||||
const controller = self.get('topicController');
|
||||
}).then(() => {
|
||||
const controller = this._topicController;
|
||||
if (controller) {
|
||||
const postNumbers = Object.keys(newTimings).map(function(v) {
|
||||
return parseInt(v, 10);
|
||||
});
|
||||
const postNumbers = Object.keys(newTimings).map(v => parseInt(v, 10));
|
||||
controller.readPosts(topicId, postNumbers);
|
||||
}
|
||||
});
|
||||
} else if (this.get('anonFlushCallback')) {
|
||||
} else if (this._anonCallback) {
|
||||
// Anonymous viewer - save to localStorage
|
||||
const storage = this.get('keyValueStore');
|
||||
const storage = this.keyValueStore;
|
||||
|
||||
// Save total time
|
||||
const existingTime = storage.getInt('anon-topic-time');
|
||||
storage.setItem('anon-topic-time', existingTime + this.get('topicTime'));
|
||||
storage.setItem('anon-topic-time', existingTime + this._topicTime);
|
||||
|
||||
// Save unique topic IDs up to a max
|
||||
let topicIds = storage.get('anon-topic-ids');
|
||||
@ -158,64 +146,47 @@ const ScreenTrack = Ember.Object.extend({
|
||||
}
|
||||
|
||||
// Inform the observer
|
||||
this.get('anonFlushCallback')();
|
||||
this._anonCallback();
|
||||
|
||||
// No need to call controller.readPosts()
|
||||
}
|
||||
|
||||
this.set('topicTime', 0);
|
||||
this._topicTime = 0;
|
||||
}
|
||||
this.set('lastFlush', 0);
|
||||
},
|
||||
|
||||
this._lastFlush = 0;
|
||||
}
|
||||
|
||||
tick() {
|
||||
const now = new Date().getTime();
|
||||
|
||||
// If the user hasn't scrolled the browser in a long time, stop tracking time read
|
||||
const sinceScrolled = new Date().getTime() - this.get('lastScrolled');
|
||||
const sinceScrolled = now - this._lastScrolled;
|
||||
if (sinceScrolled > PAUSE_UNLESS_SCROLLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = new Date().getTime() - this.get('lastTick');
|
||||
this.set('lastFlush', this.get('lastFlush') + diff);
|
||||
this.set('lastTick', new Date().getTime());
|
||||
const diff = now - this._lastTick;
|
||||
this._lastFlush += diff;
|
||||
this._lastTick = now;
|
||||
|
||||
const totalTimings = this.get('totalTimings'), timings = this.get('timings');
|
||||
const nextFlush = Discourse.SiteSettings.flush_timings_secs * 1000;
|
||||
const totalTimings = this._totalTimings;
|
||||
const timings = this._timings;
|
||||
const nextFlush = this.siteSettings.flush_timings_secs * 1000;
|
||||
|
||||
// rush new post numbers
|
||||
const rush = _.any(_.filter(timings, function(t){return t.time>0;}), function(t){
|
||||
return !totalTimings[t.postNumber];
|
||||
const rush = Object.keys(timings).some(postNumber => {
|
||||
return timings[postNumber] > 0 && !totalTimings[postNumber];
|
||||
});
|
||||
|
||||
if (this.get('lastFlush') > nextFlush || rush) {
|
||||
if (this._lastFlush > nextFlush || rush) {
|
||||
this.flush();
|
||||
}
|
||||
|
||||
// Don't track timings if we're not in focus
|
||||
if (!Discourse.get("hasFocus")) return;
|
||||
|
||||
this.set('topicTime', this.get('topicTime') + diff);
|
||||
const docViewTop = $(window).scrollTop() + $('header').height(),
|
||||
docViewBottom = docViewTop + $(window).height();
|
||||
this._topicTime += diff;
|
||||
|
||||
// TODO: Eyeline has a smarter more accurate function here. It's bad to do jQuery
|
||||
// in a model like component, so we should refactor this out later.
|
||||
_.each(this.get('timings'),function(timing,id) {
|
||||
const $element = $(id);
|
||||
if ($element.length === 1) {
|
||||
const elemTop = $element.offset().top,
|
||||
elemBottom = elemTop + $element.height();
|
||||
|
||||
// If part of the element is on the screen, increase the counter
|
||||
if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) {
|
||||
timing.time = timing.time + diff;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._onscreen.forEach(postNumber => timings[postNumber] = (timings[postNumber] || 0) + diff);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ScreenTrack.reopenClass(Singleton);
|
||||
export default ScreenTrack;
|
||||
}
|
||||
|
||||
201
app/assets/javascripts/discourse/lib/transform-post.js.es6
Normal file
201
app/assets/javascripts/discourse/lib/transform-post.js.es6
Normal file
@ -0,0 +1,201 @@
|
||||
function actionDescription(action, acted, count) {
|
||||
if (acted) {
|
||||
if (count <= 1) {
|
||||
return I18n.t(`post.actions.by_you.${action}`);
|
||||
} else {
|
||||
return I18n.t(`post.actions.by_you_and_others.${action}`, { count: count - 1 });
|
||||
}
|
||||
} else {
|
||||
return I18n.t(`post.actions.by_others.${action}`, { count });
|
||||
}
|
||||
}
|
||||
|
||||
const _additionalAttributes = [];
|
||||
|
||||
export function includeAttributes(...attributes) {
|
||||
attributes.forEach(a => _additionalAttributes.push(a));
|
||||
}
|
||||
|
||||
export function transformBasicPost(post) {
|
||||
// Note: it can be dangerous to not use `get` in Ember code, but this is significantly
|
||||
// faster and has tests to confirm it works. We only call `get` when the property is a CP
|
||||
return {
|
||||
id: post.id,
|
||||
hidden: post.hidden,
|
||||
deleted: post.get('deleted'),
|
||||
deleted_at: post.deleted_at,
|
||||
user_deleted: post.user_deleted,
|
||||
isDeleted: post.deleted_at || post.user_deleted,
|
||||
deletedByAvatarTemplate: null,
|
||||
deletedByUsername: null,
|
||||
primary_group_name: post.primary_group_name,
|
||||
wiki: post.wiki,
|
||||
firstPost: post.post_number === 1,
|
||||
post_number: post.post_number,
|
||||
cooked: post.cooked,
|
||||
via_email: post.via_email,
|
||||
user_id: post.user_id,
|
||||
usernameUrl: Discourse.getURL(`/users/${post.username}`),
|
||||
username: post.username,
|
||||
avatar_template: post.avatar_template,
|
||||
bookmarked: post.bookmarked,
|
||||
yours: post.yours,
|
||||
shareUrl: post.get('shareUrl'),
|
||||
staff: post.staff,
|
||||
admin: post.admin,
|
||||
moderator: post.moderator,
|
||||
new_user: post.trust_level === 0,
|
||||
name: post.name,
|
||||
user_title: post.user_title,
|
||||
created_at: post.created_at,
|
||||
updated_at: post.updated_at,
|
||||
canDelete: post.can_delete,
|
||||
canRecover: post.can_recover,
|
||||
canEdit: post.can_edit,
|
||||
canFlag: !Ember.isEmpty(post.get('flagsAvailable')),
|
||||
version: post.version,
|
||||
canRecoverTopic: false,
|
||||
canDeletedTopic: false,
|
||||
canViewEditHistory: post.can_view_edit_history,
|
||||
canWiki: post.can_wiki,
|
||||
showLike: false,
|
||||
liked: false,
|
||||
canToggleLike: false,
|
||||
likeCount: false,
|
||||
actionsSummary: null,
|
||||
read: post.read,
|
||||
replyToUsername: null,
|
||||
replyToAvatarTemplate: null,
|
||||
reply_to_post_number: post.reply_to_post_number,
|
||||
cooked_hidden: !!post.cooked_hidden,
|
||||
expandablePost: false,
|
||||
replyCount: post.reply_count,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function transformPost(currentUser, site, post, prevPost, nextPost) {
|
||||
// Note: it can be dangerous to not use `get` in Ember code, but this is significantly
|
||||
// faster and has tests to confirm it works. We only call `get` when the property is a CP
|
||||
const postType = post.post_type;
|
||||
const postTypes = site.post_types;
|
||||
const topic = post.topic;
|
||||
const details = topic.get('details');
|
||||
|
||||
const postAtts = transformBasicPost(post);
|
||||
|
||||
postAtts.topicId = topic.id;
|
||||
postAtts.topicOwner = details.created_by.id === post.user_id;
|
||||
postAtts.post_type = postType;
|
||||
postAtts.via_email = post.via_email;
|
||||
postAtts.isModeratorAction = postType === postTypes.moderator_action;
|
||||
postAtts.isWhisper = postType === postTypes.whisper;
|
||||
postAtts.isSmallAction = postType === postTypes.small_action;
|
||||
postAtts.canBookmark = !!currentUser;
|
||||
postAtts.canManage = currentUser && currentUser.get('canManageTopic');
|
||||
postAtts.canViewRawEmail = currentUser && (currentUser.id === post.user_id || currentUser.staff);
|
||||
postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic;
|
||||
postAtts.isWarning = topic.is_warning;
|
||||
postAtts.links = post.get('internalLinks');
|
||||
postAtts.replyDirectlyBelow = nextPost && nextPost.reply_to_post_number === post.post_number;
|
||||
postAtts.replyDirectlyAbove = prevPost && post.reply_to_post_number === prevPost.post_number;
|
||||
postAtts.linkCounts = post.link_counts;
|
||||
postAtts.actionCode = post.action_code;
|
||||
postAtts.actionCodeWho = post.action_code_who;
|
||||
postAtts.userCustomFields = post.user_custom_fields;
|
||||
postAtts.topicUrl = topic.get('url');
|
||||
|
||||
const showPMMap = topic.archetype === 'private_message' && post.post_number === 1;
|
||||
if (showPMMap) {
|
||||
postAtts.showPMMap = true;
|
||||
postAtts.allowedGroups = details.allowed_groups;
|
||||
postAtts.allowedUsers = details.allowed_users;
|
||||
postAtts.canRemoveAllowedUsers = details.can_remove_allowed_users;
|
||||
postAtts.canInvite = details.can_invite_to;
|
||||
}
|
||||
|
||||
const showTopicMap = showPMMap || (post.post_number === 1 && topic.archetype === 'regular' && topic.posts_count > 1);
|
||||
if (showTopicMap) {
|
||||
postAtts.showTopicMap = true;
|
||||
postAtts.topicCreatedAt = topic.created_at;
|
||||
postAtts.createdByUsername = details.created_by.username;
|
||||
postAtts.createdByAvatarTemplate = details.created_by.avatar_template;
|
||||
|
||||
postAtts.lastPostUrl = topic.get('lastPostUrl');
|
||||
postAtts.lastPostUsername = details.last_poster.username;
|
||||
postAtts.lastPostAvatarTemplate = details.last_poster.avatar_template;
|
||||
postAtts.lastPostAt = topic.last_posted_at;
|
||||
|
||||
postAtts.topicReplyCount = topic.get('replyCount');
|
||||
postAtts.topicViews = topic.views;
|
||||
postAtts.topicViewsHeat = topic.get('viewsHeat');
|
||||
|
||||
postAtts.participantCount = topic.participant_count;
|
||||
postAtts.topicLikeCount = topic.like_count;
|
||||
postAtts.topicLinks = details.links;
|
||||
if (postAtts.topicLinks) {
|
||||
postAtts.topicLinkLength = details.links.length;
|
||||
}
|
||||
postAtts.topicPostsCount = topic.posts_count;
|
||||
|
||||
postAtts.participants = details.participants;
|
||||
|
||||
const postStream = topic.get('postStream');
|
||||
postAtts.userFilters = postStream.userFilters;
|
||||
postAtts.topicSummaryEnabled = postStream.summary;
|
||||
postAtts.topicWordCount = topic.word_count;
|
||||
postAtts.hasTopicSummary = topic.has_summary;
|
||||
}
|
||||
|
||||
if (postAtts.isDeleted) {
|
||||
postAtts.deletedByAvatarTemplate = post.get('postDeletedBy.avatar_template');
|
||||
postAtts.deletedByUsername = post.get('postDeletedBy.username');
|
||||
}
|
||||
|
||||
const replyToUser = post.get('reply_to_user');
|
||||
if (replyToUser) {
|
||||
postAtts.replyToUsername = replyToUser.username;
|
||||
postAtts.replyToAvatarTemplate = replyToUser.avatar_template;
|
||||
}
|
||||
|
||||
if (post.actions_summary) {
|
||||
postAtts.actionsSummary = post.actions_summary.filter(a => {
|
||||
return a.actionType.name_key !== 'like' && a.count > 0;
|
||||
}).map(a => {
|
||||
const acted = a.acted;
|
||||
const action = a.actionType.name_key;
|
||||
const count = a.count;
|
||||
|
||||
return { id: a.id,
|
||||
postId: post.id,
|
||||
action,
|
||||
acted,
|
||||
count,
|
||||
canUndo: a.can_undo,
|
||||
canDeferFlags: a.can_defer_flags,
|
||||
description: actionDescription(action, acted, count) };
|
||||
});
|
||||
}
|
||||
|
||||
const likeAction = post.likeAction;
|
||||
if (likeAction) {
|
||||
postAtts.liked = likeAction.acted;
|
||||
postAtts.canToggleLike = likeAction.get('canToggle');
|
||||
postAtts.showLike = postAtts.liked || postAtts.canToggleLike;
|
||||
postAtts.likeCount = likeAction.count;
|
||||
}
|
||||
|
||||
if (postAtts.post_number === 1) {
|
||||
postAtts.canRecoverTopic = topic.deleted_at && details.can_recover;
|
||||
postAtts.canDeleteTopic = !topic.deleted_at && details.can_delete;
|
||||
postAtts.expandablePost = topic.expandable_first_post;
|
||||
} else {
|
||||
postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover;
|
||||
postAtts.canDelete = !postAtts.isDeleted && postAtts.canDelete;
|
||||
}
|
||||
|
||||
_additionalAttributes.forEach(a => postAtts[a] = post[a]);
|
||||
|
||||
return postAtts;
|
||||
}
|
||||
@ -14,10 +14,9 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
/**
|
||||
Jumps to a particular post in the stream
|
||||
**/
|
||||
jumpToPost: function(postNumber, opts) {
|
||||
const holderId = `.post-cloak[data-post-number=${postNumber}]`;
|
||||
const offset = function() {
|
||||
|
||||
jumpToPost(postNumber, opts) {
|
||||
const holderId = `#post_${postNumber}`;
|
||||
const offset = () => {
|
||||
const $header = $('header');
|
||||
const $title = $('#topic-title');
|
||||
const windowHeight = $(window).height() - $title.height();
|
||||
@ -26,8 +25,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
return $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset);
|
||||
};
|
||||
|
||||
|
||||
Em.run.schedule('afterRender', function() {
|
||||
Em.run.schedule('afterRender', () => {
|
||||
if (postNumber === 1) {
|
||||
$(window).scrollTop(0);
|
||||
return;
|
||||
@ -37,21 +35,18 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
const holder = $(holderId);
|
||||
|
||||
if (holder.length > 0 && opts && opts.skipIfOnScreen){
|
||||
|
||||
// if we are on screen skip
|
||||
const elementTop = lockon.elementTop(),
|
||||
scrollTop = $(window).scrollTop(),
|
||||
windowHeight = $(window).height()-offset(),
|
||||
height = holder.height();
|
||||
scrollTop = $(window).scrollTop(),
|
||||
windowHeight = $(window).height()-offset(),
|
||||
height = holder.height();
|
||||
|
||||
if (elementTop > scrollTop &&
|
||||
(elementTop + height) < (scrollTop + windowHeight)) {
|
||||
if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lockon.lock();
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
@ -81,7 +76,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
},
|
||||
|
||||
// Scroll to the same page, different anchor
|
||||
scrollToId: function(id) {
|
||||
scrollToId(id) {
|
||||
if (Em.isEmpty(id)) { return; }
|
||||
|
||||
_jumpScheduled = true;
|
||||
|
||||
@ -3,20 +3,6 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default RestModel.extend({
|
||||
|
||||
// Description for the action
|
||||
description: function() {
|
||||
const action = this.get('actionType.name_key');
|
||||
if (this.get('acted')) {
|
||||
if (this.get('count') <= 1) {
|
||||
return I18n.t('post.actions.by_you.' + action);
|
||||
} else {
|
||||
return I18n.t('post.actions.by_you_and_others.' + action, { count: this.get('count') - 1 });
|
||||
}
|
||||
} else {
|
||||
return I18n.t('post.actions.by_others.' + action, { count: this.get('count') });
|
||||
}
|
||||
}.property('count', 'acted', 'actionType'),
|
||||
|
||||
canToggle: function() {
|
||||
return this.get('can_undo') || this.get('can_act');
|
||||
}.property('can_undo', 'can_act'),
|
||||
@ -31,7 +17,14 @@ export default RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
toggle: function(post) {
|
||||
togglePromise(post) {
|
||||
if (!this.get('acted')) {
|
||||
return this.act(post).then(() => true);
|
||||
}
|
||||
return this.undo(post).then(() => false);
|
||||
},
|
||||
|
||||
toggle(post) {
|
||||
if (!this.get('acted')) {
|
||||
this.act(post);
|
||||
return true;
|
||||
@ -42,7 +35,7 @@ export default RestModel.extend({
|
||||
},
|
||||
|
||||
// Perform this action
|
||||
act: function(post, opts) {
|
||||
act(post, opts) {
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
@ -83,37 +76,20 @@ export default RestModel.extend({
|
||||
},
|
||||
|
||||
// Undo this action
|
||||
undo: function(post) {
|
||||
undo(post) {
|
||||
this.removeAction(post);
|
||||
|
||||
// Remove our post action
|
||||
return Discourse.ajax("/post_actions/" + post.get('id'), {
|
||||
type: 'DELETE',
|
||||
data: {
|
||||
post_action_type_id: this.get('id')
|
||||
}
|
||||
}).then(function(result) {
|
||||
return post.updateActionsSummary(result);
|
||||
});
|
||||
data: { post_action_type_id: this.get('id') }
|
||||
}).then(result => post.updateActionsSummary(result));
|
||||
},
|
||||
|
||||
deferFlags: function(post) {
|
||||
const self = this;
|
||||
deferFlags(post) {
|
||||
return Discourse.ajax("/post_actions/defer_flags", {
|
||||
type: "POST",
|
||||
data: {
|
||||
post_action_type_id: this.get("id"),
|
||||
id: post.get('id')
|
||||
}
|
||||
}).then(function () {
|
||||
self.set("count", 0);
|
||||
});
|
||||
},
|
||||
|
||||
loadUsers(post) {
|
||||
return this.store.find('post-action-user', {
|
||||
id: post.get('id'),
|
||||
post_action_type_id: this.get('id')
|
||||
});
|
||||
data: { post_action_type_id: this.get("id"), id: post.get('id') }
|
||||
}).then(() => this.set('count', 0));
|
||||
}
|
||||
});
|
||||
|
||||
@ -154,7 +154,7 @@ const Composer = RestModel.extend({
|
||||
usernameLink
|
||||
});
|
||||
|
||||
if (!Discourse.Mobile.mobileView) {
|
||||
if (!this.site.mobileView) {
|
||||
const replyUsername = post.get('reply_to_user.username');
|
||||
const replyAvatarTemplate = post.get('reply_to_user.avatar_template');
|
||||
if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
|
||||
|
||||
@ -37,6 +37,7 @@ Discourse.LoginMethod.reopenClass({
|
||||
"cas",
|
||||
"twitter",
|
||||
"yahoo",
|
||||
"instagram",
|
||||
"github"
|
||||
].forEach(function(name){
|
||||
if (Discourse.SiteSettings["enable_" + name + "_logins"]) {
|
||||
|
||||
@ -7,7 +7,7 @@ const NavItem = Discourse.Model.extend({
|
||||
name = this.get('name'),
|
||||
count = this.get('count') || 0;
|
||||
|
||||
if (name === 'latest' && !Discourse.Mobile.mobileView) {
|
||||
if (name === 'latest' && !Discourse.Site.currentProp('mobileView')) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
|
||||
@ -4,21 +4,6 @@ import PostsWithPlaceholders from 'discourse/lib/posts-with-placeholders';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { loadTopicView } from 'discourse/models/topic';
|
||||
|
||||
function calcDayDiff(p1, p2) {
|
||||
if (!p1) { return; }
|
||||
|
||||
const date = p1.get('created_at');
|
||||
if (date && p2) {
|
||||
const lastDate = p2.get('created_at');
|
||||
if (lastDate) {
|
||||
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
|
||||
const days = Math.round(delta / (1000 * 60 * 60 * 24));
|
||||
|
||||
p1.set('daysSincePrevious', days);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RestModel.extend({
|
||||
_identityMap: null,
|
||||
posts: null,
|
||||
@ -295,6 +280,7 @@ export default RestModel.extend({
|
||||
if (idx !== -1) {
|
||||
stream.pushObjects(gap);
|
||||
return this.appendMore().then(() => {
|
||||
delete this.get('gaps.after')[postId];
|
||||
this.get('stream').enumerableContentDidChange();
|
||||
});
|
||||
}
|
||||
@ -377,7 +363,6 @@ export default RestModel.extend({
|
||||
|
||||
// Commit the post we staged. Call this after a save succeeds.
|
||||
commitPost(post) {
|
||||
|
||||
if (this.get('topic.id') === post.get('topic_id')) {
|
||||
if (this.get('loadedAllPosts')) {
|
||||
this.appendPost(post);
|
||||
@ -414,7 +399,6 @@ export default RestModel.extend({
|
||||
const stored = this.storePost(post);
|
||||
if (stored) {
|
||||
const posts = this.get('posts');
|
||||
calcDayDiff(posts.get('firstObject'), stored);
|
||||
posts.unshiftObject(stored);
|
||||
}
|
||||
|
||||
@ -426,7 +410,6 @@ export default RestModel.extend({
|
||||
if (stored) {
|
||||
const posts = this.get('posts');
|
||||
|
||||
calcDayDiff(stored, this.get('lastAppended'));
|
||||
if (!posts.contains(stored)) {
|
||||
if (!this.get('loadingBelow')) {
|
||||
this.get('postsWithPlaceholders').appendPost(() => posts.pushObject(stored));
|
||||
@ -445,12 +428,15 @@ export default RestModel.extend({
|
||||
removePosts(posts) {
|
||||
if (Ember.isEmpty(posts)) { return; }
|
||||
|
||||
const postIds = posts.map(p => p.get('id'));
|
||||
const identityMap = this._identityMap;
|
||||
this.get('postsWithPlaceholders').refreshAll(() => {
|
||||
const allPosts = this.get('posts');
|
||||
const postIds = posts.map(p => p.get('id'));
|
||||
const identityMap = this._identityMap;
|
||||
|
||||
this.get('stream').removeObjects(postIds);
|
||||
this.get('posts').removeObjects(posts);
|
||||
postIds.forEach(id => delete identityMap[id]);
|
||||
this.get('stream').removeObjects(postIds);
|
||||
allPosts.removeObjects(posts);
|
||||
postIds.forEach(id => delete identityMap[id]);
|
||||
});
|
||||
},
|
||||
|
||||
// Returns a post from the identity map if it's been inserted.
|
||||
@ -471,10 +457,12 @@ export default RestModel.extend({
|
||||
have no filters.
|
||||
**/
|
||||
triggerNewPostInStream(postId) {
|
||||
if (!postId) { return; }
|
||||
const resolved = Ember.RSVP.Promise.resolve();
|
||||
|
||||
if (!postId) { return resolved; }
|
||||
|
||||
// We only trigger if there are no filters active
|
||||
if (!this.get('hasNoFilters')) { return; }
|
||||
if (!this.get('hasNoFilters')) { return resolved; }
|
||||
|
||||
const loadedAllPosts = this.get('loadedAllPosts');
|
||||
|
||||
@ -482,25 +470,27 @@ export default RestModel.extend({
|
||||
this.get('stream').addObject(postId);
|
||||
if (loadedAllPosts) {
|
||||
this.set('loadingLastPost', true);
|
||||
this.findPostsByIds([postId]).then(posts => {
|
||||
return this.findPostsByIds([postId]).then(posts => {
|
||||
posts.forEach(p => this.appendPost(p));
|
||||
}).finally(() => {
|
||||
this.set('loadingLastPost', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
},
|
||||
|
||||
triggerRecoveredPost(postId) {
|
||||
const existing = this._identityMap[postId];
|
||||
|
||||
if (existing) {
|
||||
this.triggerChangedPost(postId, new Date());
|
||||
return this.triggerChangedPost(postId, new Date());
|
||||
} else {
|
||||
// need to insert into stream
|
||||
const url = "/posts/" + postId;
|
||||
const store = this.store;
|
||||
Discourse.ajax(url).then(p => {
|
||||
return Discourse.ajax(url).then(p => {
|
||||
const post = store.createRecord('post', p);
|
||||
const stream = this.get("stream");
|
||||
const posts = this.get("posts");
|
||||
@ -541,34 +531,26 @@ export default RestModel.extend({
|
||||
const url = "/posts/" + postId;
|
||||
const store = this.store;
|
||||
|
||||
Discourse.ajax(url).then(p => {
|
||||
return Discourse.ajax(url).then(p => {
|
||||
this.storePost(store.createRecord('post', p));
|
||||
}).catch(() => {
|
||||
this.removePosts([existing]);
|
||||
});
|
||||
}
|
||||
return Ember.RSVP.Promise.resolve();
|
||||
},
|
||||
|
||||
triggerChangedPost(postId, updatedAt) {
|
||||
if (!postId) { return; }
|
||||
const resolved = Ember.RSVP.Promise.resolve();
|
||||
if (!postId) { return resolved; }
|
||||
|
||||
const existing = this._identityMap[postId];
|
||||
if (existing && existing.updated_at !== updatedAt) {
|
||||
const url = "/posts/" + postId;
|
||||
const store = this.store;
|
||||
Discourse.ajax(url).then(p => this.storePost(store.createRecord('post', p)));
|
||||
return Discourse.ajax(url).then(p => this.storePost(store.createRecord('post', p)));
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the "thread" of posts in the history of a post.
|
||||
findReplyHistory(post) {
|
||||
const url = `/posts/${post.get('id')}/reply-history.json?max_replies=${Discourse.SiteSettings.max_reply_history}`;
|
||||
const store = this.store;
|
||||
return Discourse.ajax(url).then(result => {
|
||||
return result.map(p => this.storePost(store.createRecord('post', p)));
|
||||
}).then(replyHistory => {
|
||||
post.set('replyHistory', replyHistory);
|
||||
});
|
||||
return resolved;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -7,10 +7,6 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const Post = RestModel.extend({
|
||||
|
||||
init() {
|
||||
this.set('replyHistory', []);
|
||||
},
|
||||
|
||||
@computed()
|
||||
siteSettings() {
|
||||
// TODO: Remove this once one instantiate all `Discourse.Post` models via the store.
|
||||
@ -35,11 +31,6 @@ const Post = RestModel.extend({
|
||||
deletedViaTopic: Em.computed.and('firstPost', 'topic.deleted_at'),
|
||||
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
|
||||
notDeleted: Em.computed.not('deleted'),
|
||||
userDeleted: Em.computed.empty('user_id'),
|
||||
|
||||
hasTimeGap: function() {
|
||||
return (this.get('daysSincePrevious') || 0) > Discourse.SiteSettings.show_time_gap_days;
|
||||
}.property('daysSincePrevious'),
|
||||
|
||||
showName: function() {
|
||||
const name = this.get('name');
|
||||
@ -68,25 +59,13 @@ const Post = RestModel.extend({
|
||||
|
||||
usernameUrl: url('username', '/users/%@'),
|
||||
|
||||
showUserReplyTab: function() {
|
||||
return this.get('reply_to_user') && (
|
||||
!Discourse.SiteSettings.suppress_reply_directly_above ||
|
||||
this.get('reply_to_post_number') < (this.get('post_number') - 1)
|
||||
);
|
||||
}.property('reply_to_user', 'reply_to_post_number', 'post_number'),
|
||||
|
||||
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),
|
||||
hasHistory: Em.computed.gt('version', 1),
|
||||
|
||||
canViewRawEmail: function() {
|
||||
return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff');
|
||||
}.property("user_id"),
|
||||
|
||||
updatePostField(field, value) {
|
||||
const data = {};
|
||||
data[field] = value;
|
||||
|
||||
Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => {
|
||||
return Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => {
|
||||
this.set(field, value);
|
||||
this.incrementProperty("version");
|
||||
}).catch(popupAjaxError);
|
||||
@ -97,9 +76,6 @@ const Post = RestModel.extend({
|
||||
return this.get('link_counts').filterProperty('internal').filterProperty('title');
|
||||
}.property('link_counts.@each.internal'),
|
||||
|
||||
// Edits are the version - 1, so version 2 = 1 edit
|
||||
editCount: function() { return this.get('version') - 1; }.property('version'),
|
||||
|
||||
flagsAvailable: function() {
|
||||
const post = this;
|
||||
return Discourse.Site.currentProp('flagTypes').filter(function(item) {
|
||||
@ -107,17 +83,6 @@ const Post = RestModel.extend({
|
||||
});
|
||||
}.property('actions_summary.@each.can_act'),
|
||||
|
||||
actionsWithoutLikes: function() {
|
||||
if (!!Ember.isEmpty(this.get('actions_summary'))) return null;
|
||||
|
||||
return this.get('actions_summary').filter(function(i) {
|
||||
if (i.get('count') === 0) return false;
|
||||
if (i.get('actionType.name_key') === 'like') { return false; }
|
||||
if (i.get('users') && i.get('users').length > 0) return true;
|
||||
return !i.get('hidden');
|
||||
});
|
||||
}.property('actions_summary.@each.users', 'actions_summary.@each.count'),
|
||||
|
||||
afterUpdate(res) {
|
||||
if (res.category) {
|
||||
Discourse.Site.current().updateCategory(res.category);
|
||||
@ -246,10 +211,6 @@ const Post = RestModel.extend({
|
||||
let value = otherPost[key],
|
||||
oldValue = self[key];
|
||||
|
||||
if (key === "replyHistory") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) { value = null; }
|
||||
if (!oldValue) { oldValue = null; }
|
||||
|
||||
@ -267,56 +228,9 @@ const Post = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// Load replies to this post
|
||||
loadReplies() {
|
||||
if(this.get('loadingReplies')){
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('loadingReplies', true);
|
||||
this.set('replies', []);
|
||||
|
||||
const self = this;
|
||||
return Discourse.ajax("/posts/" + (this.get('id')) + "/replies")
|
||||
.then(function(loaded) {
|
||||
const replies = self.get('replies');
|
||||
_.each(loaded,function(reply) {
|
||||
const post = Discourse.Post.create(reply);
|
||||
post.set('topic', self.get('topic'));
|
||||
replies.pushObject(post);
|
||||
});
|
||||
})
|
||||
['finally'](function(){
|
||||
self.set('loadingReplies', false);
|
||||
});
|
||||
},
|
||||
|
||||
// Whether to show replies directly below
|
||||
showRepliesBelow: function() {
|
||||
const replyCount = this.get('reply_count');
|
||||
|
||||
// We don't show replies if there aren't any
|
||||
if (replyCount === 0) return false;
|
||||
|
||||
// Always show replies if the setting `suppress_reply_directly_below` is false.
|
||||
if (!Discourse.SiteSettings.suppress_reply_directly_below) return true;
|
||||
|
||||
// Always show replies if there's more than one
|
||||
if (replyCount > 1) return true;
|
||||
|
||||
// If we have *exactly* one reply, we have to consider if it's directly below us
|
||||
const topic = this.get('topic');
|
||||
return !topic.isReplyDirectlyBelow(this);
|
||||
|
||||
}.property('reply_count'),
|
||||
|
||||
expandHidden() {
|
||||
const self = this;
|
||||
return Discourse.ajax("/posts/" + this.get('id') + "/cooked.json").then(function (result) {
|
||||
self.setProperties({
|
||||
cooked: result.cooked,
|
||||
cooked_hidden: false
|
||||
});
|
||||
return Discourse.ajax("/posts/" + this.get('id') + "/cooked.json").then(result => {
|
||||
this.setProperties({ cooked: result.cooked, cooked_hidden: false });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -37,7 +37,9 @@ function findAndRemoveMap(type, id) {
|
||||
flushMap();
|
||||
|
||||
export default Ember.Object.extend({
|
||||
_plurals: {},
|
||||
_plurals: {'post-reply': 'post-replies',
|
||||
'post-reply-history': 'post_reply_histories'},
|
||||
|
||||
pluralize(thing) {
|
||||
return this._plurals[thing] || thing + "s";
|
||||
},
|
||||
|
||||
@ -34,12 +34,6 @@ const TopicDetails = RestModel.extend({
|
||||
this.set('loaded', true);
|
||||
},
|
||||
|
||||
fewParticipants: function() {
|
||||
if (!!Ember.isEmpty(this.get('participants'))) return null;
|
||||
return this.get('participants').slice(0, 3);
|
||||
}.property('participants'),
|
||||
|
||||
|
||||
notificationReasonText: function() {
|
||||
var level = this.get('notification_level');
|
||||
if(typeof level !== 'number'){
|
||||
@ -68,13 +62,13 @@ const TopicDetails = RestModel.extend({
|
||||
},
|
||||
|
||||
removeAllowedUser(user) {
|
||||
var users = this.get('allowed_users'),
|
||||
username = user.get('username');
|
||||
const users = this.get('allowed_users');
|
||||
const username = user.get('username');
|
||||
|
||||
Discourse.ajax("/t/" + this.get('topic.id') + "/remove-allowed-user", {
|
||||
return Discourse.ajax("/t/" + this.get('topic.id') + "/remove-allowed-user", {
|
||||
type: 'PUT',
|
||||
data: { username: username }
|
||||
}).then(function() {
|
||||
}).then(() => {
|
||||
users.removeObject(users.findProperty('username', username));
|
||||
});
|
||||
}
|
||||
|
||||
@ -224,34 +224,37 @@ const Topic = RestModel.extend({
|
||||
.then(function () { self.set('archetype', 'regular'); });
|
||||
},
|
||||
|
||||
estimatedReadingTime: function() {
|
||||
const wordCount = this.get('word_count');
|
||||
if (!wordCount) return;
|
||||
|
||||
return Math.floor(wordCount / Discourse.SiteSettings.read_time_word_count);
|
||||
}.property('word_count'),
|
||||
|
||||
toggleBookmark() {
|
||||
if (this.get("bookmarking")) { return; }
|
||||
if (this.get('bookmarking')) { return Ember.RSVP.Promise.resolve(); }
|
||||
this.set("bookmarking", true);
|
||||
|
||||
const self = this,
|
||||
stream = this.get('postStream'),
|
||||
posts = Em.get(stream, 'posts'),
|
||||
firstPost = posts && posts[0] && posts[0].get('post_number') === 1 && posts[0],
|
||||
bookmark = !this.get('bookmarked'),
|
||||
path = bookmark ? '/bookmark' : '/remove_bookmarks';
|
||||
const stream = this.get('postStream');
|
||||
const posts = Em.get(stream, 'posts');
|
||||
const firstPost = posts && posts[0] && posts[0].get('post_number') === 1 && posts[0];
|
||||
const bookmark = !this.get('bookmarked');
|
||||
const path = bookmark ? '/bookmark' : '/remove_bookmarks';
|
||||
|
||||
const toggleBookmarkOnServer = function() {
|
||||
return Discourse.ajax('/t/' + self.get('id') + path, {
|
||||
type: 'PUT',
|
||||
}).then(function() {
|
||||
self.toggleProperty('bookmarked');
|
||||
if (bookmark && firstPost) { firstPost.set('bookmarked', true); }
|
||||
if (!bookmark && posts) {
|
||||
posts.forEach((post) => post.get('bookmarked') && post.set('bookmarked', false));
|
||||
const toggleBookmarkOnServer = () => {
|
||||
return Discourse.ajax(`/t/${this.get('id')}${path}`, { type: 'PUT' }).then(() => {
|
||||
this.toggleProperty('bookmarked');
|
||||
if (bookmark && firstPost) {
|
||||
firstPost.set('bookmarked', true);
|
||||
return [firstPost.id];
|
||||
}
|
||||
}).catch(function(error) {
|
||||
if (!bookmark && posts) {
|
||||
|
||||
const updated = [];
|
||||
posts.forEach(post => {
|
||||
if (post.get('bookmarked')) {
|
||||
post.set('bookmarked', false);
|
||||
updated.push(post.get('id'));
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
return [];
|
||||
}).catch(error => {
|
||||
let showGenericError = true;
|
||||
if (error && error.responseText) {
|
||||
try {
|
||||
@ -265,28 +268,26 @@ const Topic = RestModel.extend({
|
||||
}
|
||||
|
||||
throw error;
|
||||
}).finally(function() {
|
||||
self.set("bookmarking", false);
|
||||
});
|
||||
}).finally(() => this.set('bookmarking', false));
|
||||
};
|
||||
|
||||
let unbookmarkedPosts = [];
|
||||
const unbookmarkedPosts = [];
|
||||
if (!bookmark && posts) {
|
||||
posts.forEach((post) => post.get('bookmarked') && unbookmarkedPosts.push(post));
|
||||
posts.forEach(post => post.get('bookmarked') && unbookmarkedPosts.push(post));
|
||||
}
|
||||
|
||||
if (unbookmarkedPosts.length > 1) {
|
||||
return bootbox.confirm(
|
||||
I18n.t("bookmarks.confirm_clear"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function (confirmed) {
|
||||
if (confirmed) { return toggleBookmarkOnServer(); }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return toggleBookmarkOnServer();
|
||||
}
|
||||
return new Ember.RSVP.Promise(resolve => {
|
||||
if (unbookmarkedPosts.length > 1) {
|
||||
bootbox.confirm(
|
||||
I18n.t("bookmarks.confirm_clear"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
confirmed => confirmed ? toggleBookmarkOnServer().then(resolve) : resolve()
|
||||
);
|
||||
} else {
|
||||
toggleBookmarkOnServer().then(resolve);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createInvite(emailOrUsername, groupNames) {
|
||||
@ -390,25 +391,6 @@ const Topic = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// Is the reply to a post directly below it?
|
||||
isReplyDirectlyBelow(post) {
|
||||
const posts = this.get('postStream.posts');
|
||||
const postNumber = post.get('post_number');
|
||||
if (!posts) return;
|
||||
|
||||
const postBelow = posts[posts.indexOf(post) + 1];
|
||||
|
||||
// If the post directly below's reply_to_post_number is our post number or we are quoted,
|
||||
// it's considered directly below.
|
||||
//
|
||||
// TODO: we don't carry information about quoting, this leaves this code fairly fragile
|
||||
// instead we should start shipping quote meta data with posts, but this will add at least
|
||||
// 1 query to the topics page
|
||||
//
|
||||
return postBelow && (postBelow.get('reply_to_post_number') === postNumber ||
|
||||
postBelow.get('cooked').indexOf('data-post="'+ postNumber + '"') >= 0
|
||||
);
|
||||
},
|
||||
|
||||
hasExcerpt: Em.computed.notEmpty('excerpt'),
|
||||
|
||||
|
||||
@ -158,15 +158,17 @@ const User = RestModel.extend({
|
||||
'external_links_in_new_tab',
|
||||
'email_digests',
|
||||
'email_direct',
|
||||
'email_in_reply_to',
|
||||
'email_private_messages',
|
||||
'email_previous_replies',
|
||||
'dynamic_favicon',
|
||||
'enable_quoting',
|
||||
'disable_jump_reply',
|
||||
'automatically_unpin_topics',
|
||||
'digest_after_days',
|
||||
'digest_after_minutes',
|
||||
'new_topic_duration_minutes',
|
||||
'auto_track_topics_after_msecs'
|
||||
'auto_track_topics_after_msecs',
|
||||
'like_notification_frequency'
|
||||
].forEach(s => {
|
||||
data[s] = this.get(`user_option.${s}`);
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import DiscourseURL from 'discourse/lib/url';
|
||||
import DiscourseLocation from 'discourse/lib/discourse-location';
|
||||
import SearchService from 'discourse/services/search';
|
||||
import { startTracking, default as TopicTrackingState } from 'discourse/models/topic-tracking-state';
|
||||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
|
||||
function inject() {
|
||||
const app = arguments[0],
|
||||
@ -38,23 +39,29 @@ export default {
|
||||
const currentUser = Discourse.User.current();
|
||||
app.register('current-user:main', currentUser, { instantiate: false });
|
||||
|
||||
const tracking = TopicTrackingState.create({ messageBus, currentUser });
|
||||
app.register('topic-tracking-state:main', tracking, { instantiate: false });
|
||||
const topicTrackingState = TopicTrackingState.create({ messageBus, currentUser });
|
||||
app.register('topic-tracking-state:main', topicTrackingState, { instantiate: false });
|
||||
injectAll(app, 'topicTrackingState');
|
||||
|
||||
const site = Discourse.Site.current();
|
||||
app.register('site:main', site, { instantiate: false });
|
||||
injectAll(app, 'site');
|
||||
|
||||
app.register('site-settings:main', Discourse.SiteSettings, { instantiate: false });
|
||||
const siteSettings = Discourse.SiteSettings;
|
||||
app.register('site-settings:main', siteSettings, { instantiate: false });
|
||||
injectAll(app, 'siteSettings');
|
||||
|
||||
app.register('search-service:main', SearchService);
|
||||
injectAll(app, 'searchService');
|
||||
|
||||
app.register('session:main', Session.current(), { instantiate: false });
|
||||
const session = Session.current();
|
||||
app.register('session:main', session, { instantiate: false });
|
||||
injectAll(app, 'session');
|
||||
|
||||
const screenTrack = new ScreenTrack(topicTrackingState, siteSettings, session, currentUser);
|
||||
app.register('screen-track:main', screenTrack, { instantiate: false });
|
||||
inject(app, 'screenTrack', 'component', 'route');
|
||||
|
||||
inject(app, 'currentUser', 'component', 'route', 'controller');
|
||||
|
||||
app.register('location:discourse-location', DiscourseLocation);
|
||||
@ -63,6 +70,6 @@ export default {
|
||||
app.register('key-value-store:main', keyValueStore, { instantiate: false });
|
||||
injectAll(app, 'keyValueStore');
|
||||
|
||||
startTracking(tracking);
|
||||
startTracking(topicTrackingState);
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,10 +4,10 @@ import showModal from 'discourse/lib/show-modal';
|
||||
import OpenComposer from "discourse/mixins/open-composer";
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
function unlessReadOnly(method) {
|
||||
function unlessReadOnly(method, message) {
|
||||
return function() {
|
||||
if (this.site.get("isReadOnly")) {
|
||||
bootbox.alert(I18n.t("read_only_mode.login_disabled"));
|
||||
bootbox.alert(message);
|
||||
} else {
|
||||
this[method]();
|
||||
}
|
||||
@ -17,13 +17,15 @@ function unlessReadOnly(method) {
|
||||
const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
siteTitle: setting('title'),
|
||||
|
||||
_handleLogout() {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.destroySession().then(() => logout(this.siteSettings, this.keyValueStore));
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
logout() {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.destroySession().then(() => logout(this.siteSettings, this.keyValueStore));
|
||||
}
|
||||
},
|
||||
logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")),
|
||||
|
||||
_collectTitleTokens(tokens) {
|
||||
tokens.push(this.get('siteTitle'));
|
||||
@ -37,12 +39,6 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
return this._super();
|
||||
},
|
||||
|
||||
// This is here as a bugfix for when an Ember Cloaked view triggers
|
||||
// a scroll after a controller has been torn down. The real fix
|
||||
// should be to fix ember cloaking to not do that, but this catches
|
||||
// it safely just in case.
|
||||
postChangedRoute: Ember.K,
|
||||
|
||||
showTopicEntrance(data) {
|
||||
this.controllerFor('topic-entrance').send('show', data);
|
||||
},
|
||||
@ -89,9 +85,9 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
return true;
|
||||
},
|
||||
|
||||
showLogin: unlessReadOnly('handleShowLogin'),
|
||||
showLogin: unlessReadOnly('handleShowLogin', I18n.t("read_only_mode.login_disabled")),
|
||||
|
||||
showCreateAccount: unlessReadOnly('handleShowCreateAccount'),
|
||||
showCreateAccount: unlessReadOnly('handleShowCreateAccount', I18n.t("read_only_mode.login_disabled")),
|
||||
|
||||
showForgotPassword() {
|
||||
showModal('forgotPassword', { title: 'forgot_password.title' });
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
import { queryParams } from 'discourse/controllers/discovery-sortable';
|
||||
|
||||
// A helper to build a topic route for a filter
|
||||
@ -69,7 +68,7 @@ export default function(filter, extras) {
|
||||
|
||||
model(data, transition) {
|
||||
// attempt to stop early cause we need this to be called before .sync
|
||||
ScreenTrack.current().stop();
|
||||
this.screenTrack.stop();
|
||||
|
||||
const findOpts = filterQueryParams(data),
|
||||
findExtras = { cached: this.isPoppedState(transition) };
|
||||
|
||||
@ -75,8 +75,10 @@ const DiscourseRoute = Ember.Route.extend({
|
||||
});
|
||||
|
||||
export function cleanDOM() {
|
||||
// Close mini profiler
|
||||
$('.profiler-results .profiler-result').remove();
|
||||
|
||||
if (window.MiniProfiler) {
|
||||
window.MiniProfiler.pageTransition();
|
||||
}
|
||||
|
||||
// Close some elements that may be open
|
||||
$('header ul.icons li').removeClass('active');
|
||||
|
||||
@ -1,20 +1,38 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
beforeModel: function(transition) {
|
||||
const self = this;
|
||||
if (Discourse.User.current()) {
|
||||
// User is logged in
|
||||
self.replaceWith('discovery.latest').then(function(e) {
|
||||
Discourse.User.findByUsername(transition.queryParams.username).then((user) => {
|
||||
if (user.can_send_private_message_to_user) {
|
||||
Ember.run.next(function() {
|
||||
e.send('createNewMessageViaParams', user.username, transition.queryParams.title, transition.queryParams.body);
|
||||
});
|
||||
} else {
|
||||
bootbox.alert(I18n.t("composer.cant_send_pm", {username: user.username}));
|
||||
}
|
||||
}).catch(() => {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
if (transition.queryParams.username) {
|
||||
// send a message to user
|
||||
Discourse.User.findByUsername(transition.queryParams.username).then((user) => {
|
||||
if (user.can_send_private_message_to_user) {
|
||||
Ember.run.next(function() {
|
||||
e.send('createNewMessageViaParams', user.username, transition.queryParams.title, transition.queryParams.body);
|
||||
});
|
||||
} else {
|
||||
bootbox.alert(I18n.t("composer.cant_send_pm", {username: user.username}));
|
||||
}
|
||||
}).catch(() => {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
} else {
|
||||
// send a message to group
|
||||
Group.find(transition.queryParams.groupname).then((group) => {
|
||||
if (!group.automatic && group.mentionable) {
|
||||
Ember.run.next(function() {
|
||||
e.send('createNewMessageViaParams', group.name, transition.queryParams.title, transition.queryParams.body);
|
||||
});
|
||||
} else {
|
||||
bootbox.alert(I18n.t("composer.cant_send_pm", {username: group.name}));
|
||||
}
|
||||
}).catch(() => {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// User is not logged in
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
let isTransitioning = false,
|
||||
@ -186,7 +185,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||
topicController.set('multiSelect', false);
|
||||
topicController.unsubscribe();
|
||||
this.controllerFor('composer').set('topic', null);
|
||||
ScreenTrack.current().stop();
|
||||
this.screenTrack.stop();
|
||||
|
||||
const headerController = this.controllerFor('header');
|
||||
if (headerController) {
|
||||
@ -215,8 +214,9 @@ const TopicRoute = Discourse.Route.extend({
|
||||
controller.subscribe();
|
||||
|
||||
this.controllerFor('topic-progress').set('model', model);
|
||||
|
||||
// We reset screen tracking every time a topic is entered
|
||||
ScreenTrack.current().start(model.get('id'), controller);
|
||||
this.screenTrack.start(model.get('id'), controller);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@ export default Discourse.Route.extend({
|
||||
// HACK: Something with the way the user card intercepts clicks seems to break how the
|
||||
// transition into a user's activity works. This makes the back button work on mobile
|
||||
// where there is no user card as well as desktop where there is.
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
if (this.site.mobileView) {
|
||||
this.replaceWith('userActivity');
|
||||
} else {
|
||||
this.transitionTo('userActivity');
|
||||
|
||||
69
app/assets/javascripts/discourse/services/logs-notice.js.es6
Normal file
69
app/assets/javascripts/discourse/services/logs-notice.js.es6
Normal file
@ -0,0 +1,69 @@
|
||||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const LOGS_NOTICE_KEY = "logs-notice-text";
|
||||
|
||||
const LogsNotice = Ember.Object.extend({
|
||||
text: "",
|
||||
|
||||
@on('init')
|
||||
_setup() {
|
||||
if (!this.get('isActivated')) return;
|
||||
|
||||
const text = this.keyValueStore.getItem(LOGS_NOTICE_KEY);
|
||||
if (text) this.set('text', text);
|
||||
|
||||
this.messageBus.subscribe("/logs_error_rate_exceeded", data => {
|
||||
const duration = data.duration;
|
||||
var siteSettingLimit = 0;
|
||||
|
||||
if (duration === 'minute') {
|
||||
siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_minute;
|
||||
} else if (duration === 'hour') {
|
||||
siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_hour;
|
||||
}
|
||||
|
||||
this.set('text',
|
||||
I18n.t('logs_error_rate_exceeded_notice', {
|
||||
timestamp: moment().format("YYYY-MM-DD H:mm:ss"),
|
||||
siteSettingLimit: siteSettingLimit,
|
||||
rate: data.rate,
|
||||
duration: duration,
|
||||
url: Discourse.getURL('/logs')
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@computed('text')
|
||||
isEmpty(text) {
|
||||
return Ember.isEmpty(text);
|
||||
},
|
||||
|
||||
@computed('text')
|
||||
message(text) {
|
||||
return new Handlebars.SafeString(text);
|
||||
},
|
||||
|
||||
@computed('currentUser')
|
||||
isAdmin(currentUser) {
|
||||
return currentUser && currentUser.admin;
|
||||
},
|
||||
|
||||
@computed('isEmpty', 'isAdmin')
|
||||
hidden(isEmpty, isAdmin) {
|
||||
return !isAdmin || isEmpty;
|
||||
},
|
||||
|
||||
@observes('text')
|
||||
_updateKeyValueStore() {
|
||||
this.keyValueStore.setItem(LOGS_NOTICE_KEY, this.get('text'));
|
||||
},
|
||||
|
||||
@computed('siteSettings.alert_admins_if_errors_per_hour', 'siteSettings.alert_admins_if_errors_per_minute')
|
||||
isActivated(errorsPerHour, errorsPerMinute) {
|
||||
return errorsPerHour > 0 || errorsPerMinute > 0;
|
||||
}
|
||||
});
|
||||
|
||||
export default LogsNotice;
|
||||
@ -29,7 +29,7 @@
|
||||
{{#link-to 'user' user}}
|
||||
{{avatar user imageSize="extra_large"}}
|
||||
<div class="details clearfix">
|
||||
{{poster-name post=user}}
|
||||
<div class='username'>{{user.username}}</div>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
<div class='earned'>
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{{category-link category allowUncategorized="true"}}
|
||||
|
||||
{{#if unreadTotal}}
|
||||
<a href={{category.url}} class='badge badge-notification'>{{unreadTotal}}</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if showTopicCount}}
|
||||
<b class="topics-count">{{category.topic_count}}</b>
|
||||
{{/if}}
|
||||
@ -74,19 +74,7 @@
|
||||
{{plugin-outlet "site-map-links-last"}}
|
||||
{{/menu-links}}
|
||||
|
||||
{{#if categories}}
|
||||
<ul class="category-links clearfix">
|
||||
<li class='heading'>
|
||||
{{d-link class="heading"
|
||||
route="discovery.categories"
|
||||
class="categories-link"
|
||||
label="filters.categories.title"}}
|
||||
</li>
|
||||
{{#each categories as |c|}}
|
||||
{{hamburger-category category=c}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{mount-widget widget='hamburger-categories' args=(as-hash categories=categories)}}
|
||||
<hr>
|
||||
|
||||
{{#menu-links omitRule="true"}}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
<h3>{{fa-icon 'envelope'}} {{i18n 'private_message_info.title'}}</h3>
|
||||
<div class='participants clearfix'>
|
||||
{{#each details.allowed_groups as |ag|}}
|
||||
<div class='user group'>
|
||||
{{fa-icon 'users'}} {{#link-to "group.index" ag.name}}{{unbound ag.name}}{{/link-to}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#each details.allowed_users as |au|}}
|
||||
<div class='user'>
|
||||
{{#user-link user=au}}
|
||||
{{avatar au imageSize="small"}}
|
||||
{{unbound au.username}}
|
||||
{{/user-link}}
|
||||
{{#if details.can_remove_allowed_users}}
|
||||
<a href class='remove-invited' {{action "removeAllowedUser" au}}>{{fa-icon "times"}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if details.can_invite_to}}
|
||||
<div class='controls'>
|
||||
<button class='btn' {{action "showPrivateInvite"}}>{{i18n 'private_message_info.invite'}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -1,4 +1,3 @@
|
||||
<div class='topic-avatar'>{{fa-icon icon}}</div>
|
||||
<div class='small-action-desc'>
|
||||
{{#if post}}
|
||||
{{#if post.can_delete}}
|
||||
@ -11,8 +10,4 @@
|
||||
{{avatar post imageSize="small"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
<p>{{description}}</p>
|
||||
{{#if post.cooked}}
|
||||
<div class='custom-message'>{{{post.cooked}}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
{{#if postStream.summary}}
|
||||
<p>{{{i18n 'summary.enabled_description'}}}</p>
|
||||
<button class='btn btn-primary' {{action "toggleSummary"}}>{{i18n 'summary.disable'}}</button>
|
||||
{{else}}
|
||||
{{#if topic.estimatedReadingTime}}
|
||||
<p>{{{i18n 'summary.description_time' replyCount=topic.replyCount readingTime=topic.estimatedReadingTime}}}</p>
|
||||
{{else}}
|
||||
<p>{{{i18n 'summary.description' replyCount=topic.replyCount}}}</p>
|
||||
{{/if}}
|
||||
|
||||
<button class='btn btn-primary' {{action "toggleSummary"}}>{{i18n 'summary.enable'}}</button>
|
||||
{{/if}}
|
||||
@ -1,6 +0,0 @@
|
||||
<a href {{bind-attr class=":poster toggled"}} {{action "toggle"}} title={{unbound participant.username}}>
|
||||
{{#if showPostCount}}
|
||||
<span class='post-count'>{{unbound participant.post_count}}</span>
|
||||
{{/if}}
|
||||
{{avatar participant imageSize="medium"}}
|
||||
</a>
|
||||
@ -23,8 +23,8 @@
|
||||
|
||||
<div class='notifications'>
|
||||
{{#conditional-loading-spinner condition=loadingNotifications containerClass="spinner-container"}}
|
||||
<hr>
|
||||
{{#if notifications}}
|
||||
<hr>
|
||||
<ul>
|
||||
{{#each notifications as |n|}}
|
||||
{{notification-item notification=n}}
|
||||
|
||||
@ -27,3 +27,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet "discovery-below"}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user