Merge branch 'master' into signup-cta

Conflicts:
	app/assets/javascripts/discourse/lib/key-value-store.js.es6
This commit is contained in:
Kane York 2015-09-15 12:26:25 -07:00
commit 6be78861ca
347 changed files with 4853 additions and 2739 deletions

View File

@ -1,129 +1,27 @@
# Contributing to Discourse # Contributing to Discourse
## Before You Start ## Important note for Developers
Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first. Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
## Reporting Bugs For more information on
1. Always update to the most recent master release; the bug may already be resolved. - how to set up your development environment
- first-time project suggestions
- code conventions
- step-by-step guide for GitHub commits
2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem. **please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)**
3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org) ## Everything Else
4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**. There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below.
5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section). - [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070)
- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986)
- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
- Documentation (TBA)
6. When the bug is fixed, we will do our best to update the Discourse topic. For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on.
## Requesting New Features *Thanks for contributing!*
1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing.
2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit.
3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below).
## Contributing (Step-by-step)
1. Clone the Repo:
git clone git://github.com/discourse/discourse.git
2. Create a new Branch:
cd discourse
git checkout -b new_discourse_branch
> Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead.
3. Code
* Adhere to common conventions you see in the existing code
* Include tests, and ensure they pass
* Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate
4. Follow the Coding Conventions
* two spaces, no tabs
* no trailing whitespaces, blank lines should have no spaces
* use spaces around operators, after commas, colons, semicolons, around `{` and before `}`
* no space after `(`, `[` or before `]`, `)`
* use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }`
* prefer `class << self; def method; end` over `def self.method` for class methods
* prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks
* avoid `return` when not required
> However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome.
5. Commit
For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling.
**NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit!
6. Update your branch
```
git fetch origin
git rebase origin/master
```
7. Fork
```
git remote add mine git@github.com:<your user name>/discourse.git
```
8. Push to your remote
```
git push mine new_discourse_branch
```
9. Issue a Pull Request
Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command:
```
git fetch origin
git checkout new_discourse_branch
git rebase origin/master
git rebase -i
< the editor opens and allows you to change the commit history >
< follow the instructions on the bottom of the editor >
git push -f mine new_discourse_branch
```
In order to make a pull request,
* Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse)
* Click "Pull Request".
* Write your branch name in the branch field (this is filled with "master" by default)
* Click "Update Commit Range".
* Ensure the changesets you introduced are included in the "Commits" tab.
* Ensure that the "Files Changed" incorporate all of your changes.
* Fill in some details about your potential patch including a meaningful title.
* Click "Send pull request".
Thanks for that -- we'll get to your pull request ASAP, we love pull requests!
10. Responding to Feedback
The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own.
> Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top).
## Translations
Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics:
* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970)
[m]: http://meta.discourse.org

View File

@ -63,7 +63,8 @@ gem 'email_reply_parser'
# note: for image_optim to correctly work you need to follow # note: for image_optim to correctly work you need to follow
# https://github.com/toy/image_optim # https://github.com/toy/image_optim
gem 'image_optim' # pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade
gem 'image_optim', '0.20.2'
gem 'multi_json' gem 'multi_json'
gem 'mustache' gem 'mustache'
gem 'nokogiri' gem 'nokogiri'
@ -90,6 +91,7 @@ gem 'rinku'
gem 'sanitize' gem 'sanitize'
gem 'sass' gem 'sass'
gem 'sidekiq' gem 'sidekiq'
gem 'sidekiq-statistic'
# for sidekiq web # for sidekiq web
gem 'sinatra', require: false gem 'sinatra', require: false

View File

@ -209,7 +209,7 @@ GEM
omniauth-twitter (1.0.1) omniauth-twitter (1.0.1)
multi_json (~> 1.3) multi_json (~> 1.3)
omniauth-oauth (~> 1.0) omniauth-oauth (~> 1.0)
onebox (1.5.24) onebox (1.5.25)
moneta (~> 0.8) moneta (~> 0.8)
multi_json (~> 1.11) multi_json (~> 1.11)
mustache mustache
@ -333,6 +333,8 @@ GEM
json (~> 1.0) json (~> 1.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
redis-namespace (~> 1.5, >= 1.5.2) redis-namespace (~> 1.5, >= 1.5.2)
sidekiq-statistic (1.1.0)
sidekiq (~> 3.3, >= 3.3.4)
simple-rss (1.3.1) simple-rss (1.3.1)
simplecov (0.9.1) simplecov (0.9.1)
docile (~> 1.1.0) docile (~> 1.1.0)
@ -420,7 +422,7 @@ DEPENDENCIES
highline highline
hiredis hiredis
htmlentities htmlentities
image_optim image_optim (= 0.20.2)
librarian (>= 0.0.25) librarian (>= 0.0.25)
listen (= 0.7.3) listen (= 0.7.3)
logster logster
@ -474,6 +476,7 @@ DEPENDENCIES
seed-fu (~> 2.3.3) seed-fu (~> 2.3.3)
shoulda shoulda
sidekiq sidekiq
sidekiq-statistic
simple-rss simple-rss
simplecov simplecov
sinatra sinatra
@ -485,3 +488,6 @@ DEPENDENCIES
uglifier uglifier
unf unf
unicorn unicorn
BUNDLED WITH
1.10.6

View File

@ -22,7 +22,7 @@ export default Ember.Component.extend({
this.set("show", true); this.set("show", true);
if (!this.get("location")) { if (!this.get("location")) {
Discourse.ajax("/admin/users/ip-info.json", { Discourse.ajax("/admin/users/ip-info", {
data: { ip: this.get("ip") } data: { ip: this.get("ip") }
}).then(function (location) { }).then(function (location) {
self.set("location", Em.Object.create(location)); self.set("location", Em.Object.create(location));
@ -38,7 +38,7 @@ export default Ember.Component.extend({
"order": "trust_level DESC" "order": "trust_level DESC"
}; };
Discourse.ajax("/admin/users/total-others-with-same-ip.json", { data: data }).then(function (result) { Discourse.ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) {
self.set("totalOthersWithSameIP", result.total); self.set("totalOthersWithSameIP", result.total);
}); });

View File

@ -39,7 +39,7 @@ export default Ember.Controller.extend({
if (this.get("showingLast")) { return; } if (this.get("showingLast")) { return; }
const group = this.get("model"), const group = this.get("model"),
offset = Math.min(group.get("offset") + group.get("model.limit"), group.get("user_count")); offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
group.set("offset", offset); group.set("offset", offset);
@ -50,7 +50,7 @@ export default Ember.Controller.extend({
if (this.get("showingFirst")) { return; } if (this.get("showingFirst")) { return; }
const group = this.get("model"), const group = this.get("model"),
offset = Math.max(group.get("offset") - group.get("model.limit"), 0); offset = Math.max(group.get("offset") - group.get("limit"), 0);
group.set("offset", offset); group.set("offset", offset);

View File

@ -6,7 +6,7 @@ export default Ember.Route.extend({
actions: { actions: {
showSettings(plugin) { showSettings(plugin) {
const controller = this.controllerFor('adminSiteSettings'); const controller = this.controllerFor('adminSiteSettings');
this.transitionTo('adminSiteSettingsCategory', 'plugins').then(function() { this.transitionTo('adminSiteSettingsCategory', 'plugins').then(() => {
if (plugin) { if (plugin) {
const match = /^(.*)_enabled/.exec(plugin.get('enabled_setting')); const match = /^(.*)_enabled/.exec(plugin.get('enabled_setting'));
if (match[1]) { if (match[1]) {

View File

@ -8,9 +8,9 @@
<div class="toggle"> <div class="toggle">
<label>{{i18n 'admin.email.format'}}</label> <label>{{i18n 'admin.email.format'}}</label>
{{#if showHtml}} {{#if showHtml}}
<span>{{i18n 'admin.email.html'}}</span> | <a href='#' {{action "toggleShowHtml"}}>{{i18n 'admin.email.text'}}</a> <span>{{i18n 'admin.email.html'}}</span> | <a href {{action "toggleShowHtml"}}>{{i18n 'admin.email.text'}}</a>
{{else}} {{else}}
<a href='#' {{action "toggleShowHtml"}}>{{i18n 'admin.email.html'}}</a> | <span>{{i18n 'admin.email.text'}}</span> <a href {{action "toggleShowHtml"}}>{{i18n 'admin.email.html'}}</a> | <span>{{i18n 'admin.email.text'}}</span>
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@ -13,9 +13,9 @@
<div> <div>
<label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label> <label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label>
<div> <div>
<a {{bind-attr class=":previous showingFirst:disabled"}} {{action "previous"}}>{{fa-icon "fast-backward"}}</a> <a class="previous {{if showingFirst 'disabled'}}" {{action "previous"}}>{{fa-icon "fast-backward"}}</a>
{{currentPage}}/{{totalPages}} {{currentPage}}/{{totalPages}}
<a {{bind-attr class=":next showingLast:disabled"}} {{action "next"}}>{{fa-icon "fast-forward"}}</a> <a class="next {{if showingLast 'disabled'}}" {{action "next"}}>{{fa-icon "fast-forward"}}</a>
</div> </div>
<div class="ac-wrap clearfix"> <div class="ac-wrap clearfix">
{{#each model.members as |member|}} {{#each model.members as |member|}}
@ -28,7 +28,7 @@
<div> <div>
<label for="user-selector">{{i18n 'admin.groups.add_members'}}</label> <label for="user-selector">{{i18n 'admin.groups.add_members'}}</label>
{{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}} {{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}}
<button {{action "addMembers"}} class='btn add'>{{fa-icon "plus"}} {{i18n 'admin.groups.add'}}</button> {{d-button action="addMembers" class="add" icon="plus" label="admin.groups.add"}}
</div> </div>
{{/unless}} {{/unless}}
{{/if}} {{/if}}

View File

@ -1,10 +1,10 @@
<div> <div>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li {{bind-attr class="newSelected:active"}}> <li {{bind-attr class="newSelected:active"}}>
<a href="#" {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a> <a href {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a>
</li> </li>
<li {{bind-attr class="previousSelected:active"}}> <li {{bind-attr class="previousSelected:active"}}>
<a href="#" {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a> <a href {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a>
</li> </li>
</ul> </ul>
<div class="modal-body"> <div class="modal-body">

View File

@ -37,7 +37,7 @@
{{i18n "admin.plugins.not_enabled"}} {{i18n "admin.plugins.not_enabled"}}
{{/if}} {{/if}}
{{else}} {{else}}
{{i18n "admin.plugins.cant_disable"}} {{i18n "admin.plugins.is_enabled"}}
{{/if}} {{/if}}
</td> </td>
<td> <td>

View File

@ -19,7 +19,7 @@ export default Ember.Component.extend(StringBuffer, {
const renderActionIf = function(property, dataAttribute, text) { const renderActionIf = function(property, dataAttribute, text) {
if (!c.get(property)) { return; } if (!c.get(property)) { return; }
buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href='#' data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>"); buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>");
}; };
// TODO multi line expansion for flags // TODO multi line expansion for flags

View File

@ -1,26 +1,32 @@
import computed from "ember-addons/ember-computed-decorators";
import { observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({ export default Ember.Component.extend({
autoCloseValid: false,
limited: false, limited: false,
autoCloseValid: false,
autoCloseUnits: function() { @computed("limited")
var key = this.get("limited") ? "composer.auto_close.limited.units" autoCloseUnits(limited) {
: "composer.auto_close.all.units"; const key = limited ? "composer.auto_close.limited.units" : "composer.auto_close.all.units";
return I18n.t(key); return I18n.t(key);
}.property("limited"), },
autoCloseExamples: function() { @computed("limited")
var key = this.get("limited") ? "composer.auto_close.limited.examples" autoCloseExamples(limited) {
: "composer.auto_close.all.examples"; const key = limited ? "composer.auto_close.limited.examples" : "composer.auto_close.all.examples";
return I18n.t(key); return I18n.t(key);
}.property("limited"), },
_updateAutoCloseValid: function() { @observes("autoCloseTime", "limited")
var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited")); _updateAutoCloseValid() {
const limited = this.get("limited"),
autoCloseTime = this.get("autoCloseTime"),
isValid = this._isAutoCloseValid(autoCloseTime, limited);
this.set("autoCloseValid", isValid); this.set("autoCloseValid", isValid);
}.observes("autoCloseTime", "limited"), },
_isAutoCloseValid: function(autoCloseTime, limited) { _isAutoCloseValid(autoCloseTime, limited) {
var t = (autoCloseTime || "").toString().trim(); const t = (autoCloseTime || "").toString().trim();
if (t.length === 0) { if (t.length === 0) {
// "empty" is always valid // "empty" is always valid
return true; return true;

View File

@ -1,7 +1,12 @@
import { on } from "ember-addons/ember-computed-decorators";
export default Ember.TextField.extend({ export default Ember.TextField.extend({
becomeFocused: function() {
var input = this.get("element"); @on("didInsertElement")
becomeFocused() {
const input = this.get("element");
input.focus(); input.focus();
input.selectionStart = input.selectionEnd = input.value.length; input.selectionStart = input.selectionEnd = input.value.length;
}.on('didInsertElement') }
}); });

View File

@ -1,3 +1,4 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload"; import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, { export default Em.Component.extend(UploadMixin, {
@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, {
tagName: "span", tagName: "span",
imageIsNotASquare: false, imageIsNotASquare: false,
uploadButtonText: function() { @computed("uploading")
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture"); uploadButtonText(uploading) {
}.property("uploading"), return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
},
uploadDone(upload) { uploadDone(upload) {
this.setProperties({ this.setProperties({
imageIsNotASquare: upload.width !== upload.height, imageIsNotASquare: upload.width !== upload.height,
uploadedAvatarTemplate: upload.url, uploadedAvatarTemplate: upload.url,
custom_avatar_upload_id: upload.id, uploadedAvatarId: upload.id,
}); });
this.sendAction("done"); this.sendAction("done");
}, },
data: function() { @computed("user_id")
return { user_id: this.get("user_id") }; data(user_id) {
}.property("user_id") return { user_id };
}
}); });

View File

@ -0,0 +1,39 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
import DropdownButton from 'discourse/components/dropdown-button';
import computed from "ember-addons/ember-computed-decorators";
export default DropdownButton.extend({
buttonExtraClasses: 'no-text',
title: '',
text: iconHTML('bars') + ' ' + iconHTML('caret-down'),
classNames: ['category-notification-menu', 'category-admin-menu'],
@computed()
dropDownContent() {
const includeReorder = this.get('siteSettings.fixed_category_positions');
const items = [
{ id: 'create',
title: I18n.t('category.create'),
description: I18n.t('category.create_long'),
styleClasses: 'fa fa-plus' }
];
if (includeReorder) {
items.push({
id: 'reorder',
title: I18n.t('categories.reorder.title'),
description: I18n.t('categories.reorder.title_long'),
styleClasses: 'fa fa-random'
});
}
return items;
},
actionNames: {
create: 'createCategory',
reorder: 'reorderCategories'
},
clicked(id) {
this.sendAction('actionNames.' + id);
}
});

View File

@ -1,5 +1,7 @@
import ComboboxView from 'discourse/components/combo-box'; import ComboboxView from 'discourse/components/combo-box';
import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import computed from 'ember-addons/ember-computed-decorators';
import { observes, on } from 'ember-addons/ember-computed-decorators';
export default ComboboxView.extend({ export default ComboboxView.extend({
classNames: ['combobox category-combobox'], classNames: ['combobox category-combobox'],
@ -8,46 +10,34 @@ export default ComboboxView.extend({
valueBinding: Ember.Binding.oneWay('source'), valueBinding: Ember.Binding.oneWay('source'),
castInteger: true, castInteger: true,
content: function() { @computed("scopedCategoryId", "categories")
let scopedCategoryId = this.get('scopedCategoryId'); content(scopedCategoryId, categories) {
// Always scope to the parent of a category, if present // Always scope to the parent of a category, if present
if (scopedCategoryId) { if (scopedCategoryId) {
const scopedCat = Discourse.Category.findById(scopedCategoryId); const scopedCat = Discourse.Category.findById(scopedCategoryId);
scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id'); scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id');
} }
return this.get('categories').filter(function(c) { return categories.filter(c => {
if (scopedCategoryId && (c.get('id') !== scopedCategoryId) && (c.get('parent_category_id') !== scopedCategoryId)) { if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
return false; if (c.get('isUncategorizedCategory')) { return false; }
} return c.get('permission') === Discourse.PermissionType.FULL;
return c.get('permission') === Discourse.PermissionType.FULL && !c.get('isUncategorizedCategory');
}); });
}.property('scopedCategoryId', 'categories'), },
_setCategories: function() { @on("init")
@observes("site.sortedCategories")
_updateCategories() {
const categories = Discourse.SiteSettings.fixed_category_positions_on_create ?
Discourse.Category.list() :
Discourse.Category.listByActivity();
this.set('categories', categories);
},
if (!this.get('categories')) { @computed("rootNone")
this.set('automatic', true); none(rootNone) {
}
this._updateCategories();
}.on('init'),
_updateCategories: function() {
if (this.get('automatic')) {
this.set('categories',
Discourse.SiteSettings.fixed_category_positions_on_create ?
Discourse.Category.list() : Discourse.Category.listByActivity()
);
}
}.observes('automatic', 'site.sortedCategories'),
none: function() {
if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) { if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) {
if (this.get('rootNone')) { if (rootNone) {
return "category.none"; return "category.none";
} else { } else {
return Discourse.Category.findUncategorized(); return Discourse.Category.findUncategorized();
@ -55,10 +45,9 @@ export default ComboboxView.extend({
} else { } else {
return 'category.choose'; return 'category.choose';
} }
}.property(), },
comboTemplate(item) { comboTemplate(item) {
let category; let category;
// If we have no id, but text with the uncategorized name, we can use that badge. // If we have no id, but text with the uncategorized name, we can use that badge.
@ -79,16 +68,14 @@ export default ComboboxView.extend({
result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + "&nbsp;" + result; result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + "&nbsp;" + result;
} }
result += " <span class='topic-count'>&times; " + category.get('topic_count') + "</span>"; result += ` <span class='topic-count'>&times; ${category.get('topic_count')}</span>`;
const description = category.get('description'); const description = category.get('description');
// TODO wtf how can this be null?; // TODO wtf how can this be null?;
if (description && description !== 'null') { if (description && description !== 'null') {
result += '<div class="category-desc">' + result += `<div class="category-desc">${description.substr(0, 200)}${description.length > 200 ? '&hellip;' : ''}</div>`;
description.substr(0,200) +
(description.length > 200 ? '&hellip;' : '') +
'</div>';
} }
return result; return result;
} }

View File

@ -54,23 +54,17 @@ export default Ember.Component.extend({
})); }));
}, },
@computed()
topicTrackingState() {
return Discourse.TopicTrackingState.current();
},
@observes('topicTrackingState.incomingCount') @observes('topicTrackingState.incomingCount')
fetchLiveStats() { fetchLiveStats() {
if (!this.get('enabled')) { return; } if (!this.get('enabled')) { return; }
var self = this; LivePostCounts.find().then((stats) => {
LivePostCounts.find().then(function(stats) {
if(stats) { if(stats) {
self.set('publicTopicCount', stats.get('public_topic_count')); this.set('publicTopicCount', stats.get('public_topic_count'));
self.set('publicPostCount', stats.get('public_post_count')); this.set('publicPostCount', stats.get('public_post_count'));
if (self.get('publicTopicCount') >= self.get('requiredTopics') if (this.get('publicTopicCount') >= this.get('requiredTopics')
&& self.get('publicPostCount') >= self.get('requiredPosts')) { && this.get('publicPostCount') >= this.get('requiredPosts')) {
self.set('enabled', false); // No more checks this.set('enabled', false); // No more checks
} }
} }
}); });

View File

@ -8,9 +8,9 @@ export default Ember.Component.extend({
noText: Ember.computed.empty('translatedLabel'), noText: Ember.computed.empty('translatedLabel'),
@computed("title", "translatedLabel") @computed("title")
translatedTitle(title, translatedLabel) { translatedTitle(title) {
return title ? I18n.t(title) : translatedLabel; if (title) return I18n.t(title);
}, },
@computed("label") @computed("label")

View File

@ -1,9 +1,10 @@
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import { iconHTML } from 'discourse/helpers/fa-icon'; import { iconHTML } from 'discourse/helpers/fa-icon';
import DiscourseURL from 'discourse/lib/url'; import interceptClick from 'discourse/lib/intercept-click';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'a', tagName: 'a',
classNames: ['d-link'],
attributeBindings: ['translatedTitle:title', 'translatedTitle:aria-title', 'href'], attributeBindings: ['translatedTitle:title', 'translatedTitle:aria-title', 'href'],
@computed('path') @computed('path')
@ -14,7 +15,13 @@ export default Ember.Component.extend({
if (route) { if (route) {
const router = this.container.lookup('router:main'); const router = this.container.lookup('router:main');
if (router && router.router) { if (router && router.router) {
return router.router.generate(route, this.get('model')); const params = [route];
const model = this.get('model');
if (model) {
params.push(model);
}
return router.router.generate.apply(router.router, params);
} }
} }
@ -27,18 +34,14 @@ export default Ember.Component.extend({
if (text) return I18n.t(text); if (text) return I18n.t(text);
}, },
click() { click(e) {
const action = this.get('action'); const action = this.get('action');
if (action) { if (action) {
this.sendAction('action'); this.sendAction('action');
return false; return false;
} }
const href = this.get('href');
if (href) { return interceptClick(e);
DiscourseURL.routeTo(href);
return false;
}
return false;
}, },
render(buffer) { render(buffer) {
@ -55,7 +58,8 @@ export default Ember.Component.extend({
if (label) { if (label) {
if (icon) { buffer.push(" "); } if (icon) { buffer.push(" "); }
buffer.push(I18n.t(label)); const count = this.get('count');
buffer.push(I18n.t(label, { count }));
} }
} }

View File

@ -29,9 +29,7 @@ export default Ember.Component.extend(StringBuffer, {
buffer.push("<h4 class='title'>" + title + "</h4>"); buffer.push("<h4 class='title'>" + title + "</h4>");
} }
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>"); buffer.push(`<button class='btn standard dropdown-toggle ${this.get('buttonExtraClasses')}' data-toggle='dropdown'>${this.get('text')}</button>`);
buffer.push(this.get('text'));
buffer.push("</button>");
buffer.push("<ul class='dropdown-menu'>"); buffer.push("<ul class='dropdown-menu'>");
const contents = this.get('dropDownContent'); const contents = this.get('dropDownContent');

View File

@ -1,6 +1,7 @@
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import { buildCategoryPanel } from 'discourse/components/edit-category-panel'; import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
export default buildCategoryPanel('general', { export default buildCategoryPanel('general', {
foregroundColors: ['FFFFFF', '000000'], foregroundColors: ['FFFFFF', '000000'],
@ -31,7 +32,7 @@ export default buildCategoryPanel('general', {
categoryBadgePreview: function() { categoryBadgePreview: function() {
const category = this.get('category'); const category = this.get('category');
const c = Discourse.Category.create({ const c = Category.create({
name: category.get('categoryName'), name: category.get('categoryName'),
color: category.get('color'), color: category.get('color'),
text_color: category.get('text_color'), text_color: category.get('text_color'),
@ -45,7 +46,7 @@ export default buildCategoryPanel('general', {
// We can change the parent if there are no children // We can change the parent if there are no children
subCategories: function() { subCategories: function() {
if (Ember.isEmpty(this.get('category.id'))) { return null; } if (Ember.isEmpty(this.get('category.id'))) { return null; }
return Discourse.Category.list().filterBy('parent_category_id', this.get('category.id')); return Category.list().filterBy('parent_category_id', this.get('category.id'));
}.property('category.id'), }.property('category.id'),
showDescription: function() { showDescription: function() {

View File

@ -7,16 +7,24 @@ export default buildCategoryPanel('security', {
actions: { actions: {
editPermissions() { editPermissions() {
if (!this.get('category.is_special')) {
this.set('editingPermissions', true); this.set('editingPermissions', true);
}
}, },
addPermission(group, id) { addPermission(group, id) {
this.get('category').addPermission({group_name: group + "", if (!this.get('category.is_special')) {
permission: Discourse.PermissionType.create({id})}); this.get('category').addPermission({
group_name: group + "",
permission: Discourse.PermissionType.create({id})
});
}
}, },
removePermission(permission) { removePermission(permission) {
if (!this.get('category.is_special')) {
this.get('category').removePermission(permission); this.get('category').removePermission(permission);
}
}, },
} }
}); });

View File

@ -2,6 +2,12 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ['hamburger-panel'], classNames: ['hamburger-panel'],
@computed('currentUser.read_faq')
prioritizeFaq(readFaq) {
// If it's a custom FAQ never prioritize it
return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq;
},
@computed() @computed()
showKeyboardShortcuts() { showKeyboardShortcuts() {
return !Discourse.Mobile.mobileView && !this.capabilities.touch; return !Discourse.Mobile.mobileView && !this.capabilities.touch;
@ -22,6 +28,21 @@ export default Ember.Component.extend({
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
}, },
_lookupCount(type) {
const state = this.get('topicTrackingState');
return state ? state.lookupCount(type) : 0;
},
@computed('topicTrackingState.messageCount')
newCount() {
return this._lookupCount('new');
},
@computed('topicTrackingState.messageCount')
unreadCount() {
return this._lookupCount('unread');
},
@computed() @computed()
categories() { categories() {
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;

View File

@ -1,11 +1,24 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'li', tagName: 'li',
classNameBindings: [':header-dropdown-toggle', 'active'], classNameBindings: [':header-dropdown-toggle', 'active'],
@computed('showUser')
href(showUser) {
return showUser ? this.currentUser.get('path') : '';
},
active: Ember.computed.alias('toggleVisible'), active: Ember.computed.alias('toggleVisible'),
actions: { actions: {
toggle() { toggle() {
if (Discourse.Mobile.mobileView && this.get('mobileAction')) {
this.sendAction('mobileAction');
return;
}
if (this.siteSettings.login_required && !this.currentUser) { if (this.siteSettings.login_required && !this.currentUser) {
this.sendAction('loginAction'); this.sendAction('loginAction');
} else { } else {

View File

@ -1,4 +1,5 @@
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
import { headerHeight } from 'discourse/views/header';
const PANEL_BODY_MARGIN = 30; const PANEL_BODY_MARGIN = 30;
const mutationSupport = !!window['MutationObserver']; const mutationSupport = !!window['MutationObserver'];
@ -21,36 +22,39 @@ export default Ember.Component.extend({
const viewMode = this.get('viewMode'); const viewMode = this.get('viewMode');
const $panelBody = this.$('.panel-body'); const $panelBody = this.$('.panel-body');
let contentHeight = parseInt(this.$('.panel-body-contents').height());
if (viewMode === 'drop-down') { if (viewMode === 'drop-down') {
const $buttonPanel = $('header ul.icons'); const $buttonPanel = $('header ul.icons');
if ($buttonPanel.length === 0) { return; } if ($buttonPanel.length === 0) { return; }
const buttonPanelPos = $buttonPanel.offset(); // These values need to be set here, not in the css file - this is to deal with the
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height() - $('header.d-header').offset().top); this.$().css({ top: '100%', height: 'auto' });
const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - width);
this.$().css({ left: posLeft + "px", top: posTop + "px" });
// adjust panel height // adjust panel height
let contentHeight = parseInt(this.$('.panel-body-contents').height());
const fullHeight = parseInt($window.height()); const fullHeight = parseInt($window.height());
const offsetTop = this.$().offset().top; const offsetTop = this.$().offset().top;
const scrollTop = $window.scrollTop(); const scrollTop = $window.scrollTop();
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
} }
$panelBody.height(contentHeight); $panelBody.height(contentHeight);
$('body').addClass('drop-down-visible'); $('body').addClass('drop-down-visible');
} else { } else {
$panelBody.height('auto');
const $header = $('header.d-header'); const menuTop = headerHeight();
const headerOffset = $header.offset();
const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; let height;
const headerHeight = parseInt($header.height() + headerOffsetTop - $window.scrollTop() + 3); if ((menuTop + contentHeight) < ($(window).height() - 20)) {
this.$().css({ left: "auto", top: headerHeight + "px" }); height = contentHeight + "px";
} else {
height = $(window).height() - menuTop;
}
$panelBody.height('100%');
this.$().css({ top: menuTop + "px", height });
$('body').removeClass('drop-down-visible'); $('body').removeClass('drop-down-visible');
} }
@ -82,7 +86,11 @@ export default Ember.Component.extend({
}); });
this.performLayout(); this.performLayout();
this._watchSizeChanges(); this._watchSizeChanges();
// iOS does not handle scroll events well
if (!this.capabilities.touch) {
$(window).on('scroll.discourse-menu-panel', () => this.performLayout()); $(window).on('scroll.discourse-menu-panel', () => this.performLayout());
}
} else { } else {
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden')); Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden'));
$('html').off('click.close-menu-panel'); $('html').off('click.close-menu-panel');
@ -124,9 +132,13 @@ export default Ember.Component.extend({
clearInterval(this._resizeInterval); clearInterval(this._resizeInterval);
this._resizeInterval = setInterval(() => { this._resizeInterval = setInterval(() => {
Ember.run(() => { Ember.run(() => {
const contentHeight = parseInt(this.$('.panel-body-contents').height()); const $panelBodyContents = this.$('.panel-body-contents');
if ($panelBodyContents.length) {
const contentHeight = parseInt($panelBodyContents.height());
if (contentHeight !== this._lastHeight) { this.performLayout(); } if (contentHeight !== this._lastHeight) { this.performLayout(); }
this._lastHeight = contentHeight; this._lastHeight = contentHeight;
}
}); });
}, 500); }, 500);
} }
@ -142,7 +154,8 @@ export default Ember.Component.extend({
@on('didInsertElement') @on('didInsertElement')
_bindEvents() { _bindEvents() {
this.$().on('click.discourse-menu-panel', 'a', (e) => { this.$().on('click.discourse-menu-panel', 'a', e => {
if (e.metaKey) { return; }
if ($(e.target).data('ember-action')) { return; } if ($(e.target).data('ember-action')) { return; }
this.hide(); this.hide();
}); });
@ -150,7 +163,7 @@ export default Ember.Component.extend({
this.appEvents.on('dropdowns:closeAll', this, this.hide); this.appEvents.on('dropdowns:closeAll', this, this.hide);
this.appEvents.on('dom:clean', this, this.hide); this.appEvents.on('dom:clean', this, this.hide);
$('body').on('keydown.discourse-menu-panel', (e) => { $('body').on('keydown.discourse-menu-panel', e => {
if (e.which === 27) { if (e.which === 27) {
this.hide(); this.hide();
} }

View File

@ -1,24 +1,28 @@
/* You might be looking for navigation-item. */ /* You might be looking for navigation-item. */
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'li', tagName: 'li',
classNameBindings: ['active'], classNameBindings: ['active'],
router: function() { @computed()
router() {
return this.container.lookup('router:main'); return this.container.lookup('router:main');
}.property(), },
fullPath: function() { @computed("path")
return Discourse.getURL(this.get('path')); fullPath(path) {
}.property('path'), return Discourse.getURL(path);
},
active: function() { @computed("route", "router.url")
const route = this.get('route'); active(route) {
if (!route) { return; } if (!route) { return; }
const routeParam = this.get('routeParam'), const routeParam = this.get('routeParam'),
router = this.get('router'); router = this.get('router');
return routeParam ? router.isActive(route, routeParam) : router.isActive(route); return routeParam ? router.isActive(route, routeParam) : router.isActive(route);
}.property('router.url', 'route') }
}); });

View File

@ -1,27 +1,25 @@
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'ul', tagName: 'ul',
classNameBindings: [':nav', ':nav-pills'], classNameBindings: [':nav', ':nav-pills'],
id: 'navigation-bar', id: 'navigation-bar',
selectedNavItem: function(){
const filterMode = this.get('filterMode'),
navItems = this.get('navItems');
var item = navItems.find(function(i){
return i.get('filterMode').indexOf(filterMode) === 0;
});
@computed("filterMode", "navItems")
selectedNavItem(filterMode, navItems){
var item = navItems.find(i => i.get('filterMode').indexOf(filterMode) === 0);
return item || navItems[0]; return item || navItems[0];
}.property('filterMode'), },
closedNav: function(){ @observes("expanded")
closedNav() {
if (!this.get('expanded')) { if (!this.get('expanded')) {
this.ensureDropClosed(); this.ensureDropClosed();
} }
}.observes('expanded'), },
ensureDropClosed: function(){ ensureDropClosed() {
if (!this.get('expanded')) { if (!this.get('expanded')) {
this.set('expanded',false); this.set('expanded',false);
} }
@ -30,25 +28,23 @@ export default Ember.Component.extend({
}, },
actions: { actions: {
toggleDrop: function(){ toggleDrop() {
this.set('expanded', !this.get('expanded')); this.set('expanded', !this.get('expanded'));
var self = this;
if (this.get('expanded')) {
if (this.get('expanded')) {
DiscourseURL.appEvents.on('dom:clean', this, this.ensureDropClosed); DiscourseURL.appEvents.on('dom:clean', this, this.ensureDropClosed);
Em.run.next(function() { Em.run.next(() => {
if (!this.get('expanded')) { return; }
if (!self.get('expanded')) { return; } this.$('.drop a').on('click', () => {
this.$('.drop').hide();
self.$('.drop a').on('click', function(){ this.set('expanded', false);
self.$('.drop').hide();
self.set('expanded', false);
return true; return true;
}); });
$(window).on('click.navigation-bar', function() { $(window).on('click.navigation-bar', () => {
self.set('expanded', false); this.set('expanded', false);
return true; return true;
}); });
}); });

View File

@ -1,3 +1,4 @@
import computed from "ember-addons/ember-computed-decorators";
import StringBuffer from 'discourse/mixins/string-buffer'; import StringBuffer from 'discourse/mixins/string-buffer';
export default Ember.Component.extend(StringBuffer, { export default Ember.Component.extend(StringBuffer, {
@ -7,22 +8,23 @@ export default Ember.Component.extend(StringBuffer, {
hidden: Em.computed.not('content.visible'), hidden: Em.computed.not('content.visible'),
rerenderTriggers: ['content.count'], rerenderTriggers: ['content.count'],
title: function() { @computed("content.categoryName", "content.name")
var categoryName = this.get('content.categoryName'), title(categoryName, name) {
name = this.get('content.name'), const extra = {};
extra = {};
if (categoryName) { if (categoryName) {
name = "category"; name = "category";
extra.categoryName = categoryName; extra.categoryName = categoryName;
} }
return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
}.property("content.{categoryName,name}"),
active: function() { return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
return this.get('content.filterMode') === this.get('filterMode') || },
this.get('filterMode').indexOf(this.get('content.filterMode')) === 0;
}.property('content.filterMode', 'filterMode'), @computed("content.filterMode", "filterMode")
active(contentFilterMode, filterMode) {
return contentFilterMode === filterMode ||
filterMode.indexOf(contentFilterMode) === 0;
},
renderString(buffer) { renderString(buffer) {
const content = this.get('content'); const content = this.get('content');

View File

@ -4,17 +4,19 @@ export default Ember.Component.extend({
tagName: 'li', tagName: 'li',
classNameBindings: ['notification.read', 'notification.is_warning'], classNameBindings: ['notification.read', 'notification.is_warning'],
scope: function() { name: function() {
var notificationType = this.get("notification.notification_type"); var notificationType = this.get("notification.notification_type");
var lookup = this.site.get("notificationLookup"); var lookup = this.site.get("notificationLookup");
var name = lookup[notificationType]; return lookup[notificationType];
}.property("notification.notification_type"),
if (name === "custom") { scope: function() {
if (this.get("name") === "custom") {
return this.get("notification.data.message"); return this.get("notification.data.message");
} else { } else {
return "notifications." + name; return "notifications." + this.get("name");
} }
}.property("notification.notification_type"), }.property("name"),
url: function() { url: function() {
const it = this.get('notification'); const it = this.get('notification');
@ -57,7 +59,7 @@ export default Ember.Component.extend({
const url = this.get('url'); const url = this.get('url');
if (url) { if (url) {
buffer.push('<a href="' + url + '">' + text + '</a>'); buffer.push('<a href="' + url + '" alt="' + I18n.t('notifications.alt.' + this.get("name")) + '">' + text + '</a>');
} else { } else {
buffer.push(text); buffer.push(text);
} }

View File

@ -0,0 +1,29 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.TextField.extend({
classNameBindings: ['invalid'],
@computed('number')
value: {
get(number) {
return parseInt(number);
},
set(value) {
const num = parseInt(value);
if (isNaN(num)) {
this.set('invalid', true);
return value;
} else {
this.set('invalid', false);
this.set('number', num);
return num.toString();
}
}
},
@computed("placeholderKey")
placeholder(key) {
return key ? I18n.t(key) : "";
}
});

View File

@ -17,8 +17,8 @@ export default Ember.Component.extend(StringBuffer, {
good: Ember.computed.not("bad"), good: Ember.computed.not("bad"),
@observes("shownAt") @observes("shownAt")
bounce(shownAt) { bounce() {
if (shownAt) { if (this.get("shownAt")) {
var $elem = this.$(); var $elem = this.$();
if (!this.animateAttribute) { if (!this.animateAttribute) {
this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left'; this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left';

View File

@ -3,8 +3,8 @@ export default Ember.Component.extend({
initGaps: function(){ initGaps: function(){
this.set('loading', false); this.set('loading', false);
var before = this.get('before') === 'true', const before = this.get('before') === 'true';
gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after'); const gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after');
if (gaps) { if (gaps) {
this.set('gap', gaps[this.get('post.id')]); this.set('gap', gaps[this.get('post.id')]);
@ -16,29 +16,27 @@ export default Ember.Component.extend({
this.rerender(); this.rerender();
}.observes('post.hasGap'), }.observes('post.hasGap'),
render: function(buffer) { render(buffer) {
if (this.get('loading')) { if (this.get('loading')) {
buffer.push(I18n.t('loading')); buffer.push(I18n.t('loading'));
} else { } else {
var gapLength = this.get('gap.length'); const gapLength = this.get('gap.length');
if (gapLength) { if (gapLength) {
buffer.push(I18n.t('post.gap', {count: gapLength})); buffer.push(I18n.t('post.gap', {count: gapLength}));
} }
} }
}, },
click: function() { click() {
if (this.get('loading') || (!this.get('gap'))) { return false; } if (this.get('loading') || (!this.get('gap'))) { return false; }
this.set('loading', true); this.set('loading', true);
this.rerender(); this.rerender();
var self = this, const postStream = this.get('postStream');
postStream = this.get('postStream'), const filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter;
filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter;
filler.call(postStream, this.get('post'), this.get('gap')).then(function() { filler.call(postStream, this.get('post'), this.get('gap')).then(() => {
// hide this control after the promise is resolved this.set('gap', null);
self.set('gap', null);
}); });
return false; return false;

View File

@ -1,4 +1,4 @@
import searchForTerm from 'discourse/lib/search-for-term'; import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import showModal from 'discourse/lib/show-modal'; import showModal from 'discourse/lib/show-modal';
@ -48,18 +48,7 @@ export default Ember.Component.extend({
@computed('searchService.searchContext') @computed('searchService.searchContext')
searchContextDescription(ctx) { searchContextDescription(ctx) {
if (ctx) { return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name'));
switch(Em.get(ctx, 'type')) {
case 'topic':
return I18n.t('search.context.topic');
case 'user':
return I18n.t('search.context.user', {username: Em.get(ctx, 'user.username')});
case 'category':
return I18n.t('search.context.category', {category: Em.get(ctx, 'category.name')});
case 'private_messages':
return I18n.t('search.context.private_messages');
}
}
}, },
@observes('searchService.searchContextEnabled') @observes('searchService.searchContextEnabled')
@ -72,8 +61,8 @@ export default Ember.Component.extend({
@observes('searchService.term', 'typeFilter') @observes('searchService.term', 'typeFilter')
newSearchNeeded() { newSearchNeeded() {
this.set('noResults', false); this.set('noResults', false);
const term = (this.get('searchService.term') || '').trim(); const term = this.get('searchService.term')
if (term.length >= Discourse.SiteSettings.min_search_term_length) { if (isValidSearchTerm(term)) {
this.set('loading', true); this.set('loading', true);
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400); Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
} else { } else {
@ -145,7 +134,7 @@ export default Ember.Component.extend({
}, },
showedSearch() { showedSearch() {
$('#search-term').focus(); $('#search-term').focus().select();
}, },
showSearchHelp() { showSearchHelp() {
@ -165,8 +154,7 @@ export default Ember.Component.extend({
}, },
keyDown(e) { keyDown(e) {
const term = this.get('searchService.term'); if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
this.set('visible', false); this.set('visible', false);
this.send('fullSearch'); this.send('fullSearch');
} }

View File

@ -1,9 +1,19 @@
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import { on } from 'ember-addons/ember-computed-decorators';
import TextField from 'discourse/components/text-field'; import TextField from 'discourse/components/text-field';
export default TextField.extend({ export default TextField.extend({
@computed('searchService.searchContextEnabled') @computed('searchService.searchContextEnabled')
placeholder: function(searchContextEnabled) { placeholder(searchContextEnabled) {
return searchContextEnabled ? "" : I18n.t('search.title'); return searchContextEnabled ? "" : I18n.t('search.title');
},
focusIn() {
Em.run.later(() => this.$().select());
},
@on("didInsertElement")
becomeFocused() {
if (this.get('hasAutofocus')) this.$().focus();
} }
}); });

View File

@ -1,19 +1,10 @@
/** import computed from "ember-addons/ember-computed-decorators";
This is a custom text field that allows i18n placeholders
@class TextField
@extends Ember.TextField
@namespace Discourse
@module Discourse
**/
export default Ember.TextField.extend({ export default Ember.TextField.extend({
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'], attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'],
placeholder: function() { @computed("placeholderKey")
if (this.get('placeholderKey')) { placeholder(placeholderKey) {
return I18n.t(this.get('placeholderKey')); return placeholderKey ? I18n.t(placeholderKey) : "";
} else {
return '';
} }
}.property('placeholderKey')
}); });

View File

@ -2,6 +2,8 @@ import SmallActionComponent from 'discourse/components/small-action';
export default SmallActionComponent.extend({ export default SmallActionComponent.extend({
classNames: ['time-gap'], classNames: ['time-gap'],
classNameBindings: ['hideTimeGap::hidden'],
hideTimeGap: Em.computed.alias('postStream.hasNoFilters'),
icon: 'clock-o', icon: 'clock-o',
description: function() { description: function() {

View File

@ -1,5 +1,6 @@
import { url } from 'discourse/lib/computed'; import { url } from 'discourse/lib/computed';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import { headerHeight } from 'discourse/views/header';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ['user-menu'], classNames: ['user-menu'],
@ -17,8 +18,8 @@ export default Ember.Component.extend({
showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; }, showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; },
@observes('visible') @observes('visible')
_loadNotifications(visible) { _loadNotifications() {
if (visible) { if (this.get("visible")) {
this.refreshNotifications(); this.refreshNotifications();
} }
}, },
@ -43,13 +44,30 @@ export default Ember.Component.extend({
refreshNotifications() { refreshNotifications() {
if (this.get('loadingNotifications')) { return; } if (this.get('loadingNotifications')) { return; }
// estimate (poorly) the amount of notifications to return
var limit = Math.round(($(window).height() - headerHeight()) / 50);
// we REALLY don't want to be asking for negative counts of notifications
// less than 5 is also not that useful
if (limit < 5) { limit = 5; }
if (limit > 40) { limit = 40; }
// TODO: It's a bit odd to use the store in a component, but this one really // TODO: It's a bit odd to use the store in a component, but this one really
// wants to reach out and grab notifications // wants to reach out and grab notifications
const store = this.container.lookup('store:main'); const store = this.container.lookup('store:main');
const stale = store.findStale('notification', {recent: true}); const stale = store.findStale('notification', {recent: true, limit }, {storageKey: 'recent-notifications'});
if (stale.hasResults) { if (stale.hasResults) {
this.set('notifications', stale.results); const results = stale.results;
var content = results.get('content');
// we have to truncate to limit, otherwise we will render too much
if (content && (content.length > limit)) {
content = content.splice(0, limit);
results.set('content', content);
results.set('totalRows', limit);
}
this.set('notifications', results);
} else { } else {
this.set('loadingNotifications', true); this.set('loadingNotifications', true);
} }

View File

@ -25,7 +25,7 @@ export default TextField.extend({
dataSource: function(term) { dataSource: function(term) {
return userSearch({ return userSearch({
term: term.replace(/[^a-zA-Z0-9_]/, ''), term: term.replace(/[^a-zA-Z0-9_\-\.]/, ''),
topicId: self.get('topicId'), topicId: self.get('topicId'),
exclude: excludedUsernames(), exclude: excludedUsernames(),
includeGroups, includeGroups,

View File

@ -13,7 +13,7 @@ export default Ember.Component.extend(StringBuffer, {
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">"; iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
iconsHtml += Discourse.Utilities.avatarImg({ iconsHtml += Discourse.Utilities.avatarImg({
size: 'small', size: 'small',
avatarTemplate: u.get('avatarTemplate'), avatarTemplate: u.get('avatar_template'),
title: u.get('username') title: u.get('username')
}); });
iconsHtml += "</a>"; iconsHtml += "</a>";

View File

@ -1,21 +1,29 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from "ember-addons/ember-computed-decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
uploadedAvatarTemplate: null, @computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
saveDisabled: Em.computed.alias("uploading"), selectedUploadId(selected, system, gravatar, custom) {
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'), switch (selected) {
case "system": return system;
selectedUploadId: function() { case "gravatar": return gravatar;
switch (this.get("selected")) { default: return custom;
case "system": return this.get("system_avatar_upload_id");
case "gravatar": return this.get("gravatar_avatar_upload_id");
default: return this.get("custom_avatar_upload_id");
} }
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'), },
allowImageUpload: function() { @computed("selected", "system_avatar_template", "gravatar_avatar_template", "custom_avatar_template")
selectedAvatarTemplate(selected, system, gravatar, custom) {
switch (selected) {
case "system": return system;
case "gravatar": return gravatar;
default: return custom;
}
},
@computed()
allowImageUpload() {
return Discourse.Utilities.allowsImages(); return Discourse.Utilities.allowsImages();
}.property(), },
actions: { actions: {
useUploadedAvatar() { this.set("selected", "uploaded"); }, useUploadedAvatar() { this.set("selected", "uploaded"); },
@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
refreshGravatar() { refreshGravatar() {
this.set("gravatarRefreshDisabled", true); this.set("gravatarRefreshDisabled", true);
return Discourse return Discourse
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' }) .ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id)) .then(result => this.setProperties({
gravatar_avatar_template: result.gravatar_avatar_template,
gravatar_upload_id: result.gravatar_upload_id,
}))
.finally(() => this.set("gravatarRefreshDisabled", false)); .finally(() => this.set("gravatarRefreshDisabled", false));
} }
} }

View File

@ -39,11 +39,11 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, {
username: this.get('new_user') username: this.get('new_user')
}; };
Discourse.Topic.changeOwners(this.get('topicController.model.id'), saveOpts).then(function(result) { Discourse.Topic.changeOwners(this.get('topicController.model.id'), saveOpts).then(function() {
// success // success
self.send('closeModal'); self.send('closeModal');
self.get('topicController').send('toggleMultiSelect'); self.get('topicController').send('toggleMultiSelect');
Em.run.next(function() { DiscourseURL.routeTo(result.url); }); Em.run.next(() => { DiscourseURL.routeTo(self.get("topicController.model.url")); });
}, function() { }, function() {
// failure // failure
self.flash(I18n.t('topic.change_owner.error'), 'alert-error'); self.flash(I18n.t('topic.change_owner.error'), 'alert-error');

View File

@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import DiscourseURL from 'discourse/lib/url';
// Modal related to changing the timestamp of posts // Modal related to changing the timestamp of posts
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
@ -40,14 +41,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
actions: { actions: {
changeTimestamp: function() { changeTimestamp: function() {
this.set('saving', true); this.set('saving', true);
const self = this; const self = this,
topic = this.get('topicController.model');
Discourse.Topic.changeTimestamp( Discourse.Topic.changeTimestamp(
this.get('topicController.model.id'), topic.get('id'),
this.get('createdAt').unix() this.get('createdAt').unix()
).then(function() { ).then(function() {
self.send('closeModal'); self.send('closeModal');
self.setProperties({ date: '', time: '', saving: false }); self.setProperties({ date: '', time: '', saving: false });
Em.run.next(() => { DiscourseURL.routeTo(topic.get('url')); });
}).catch(function() { }).catch(function() {
self.flash(I18n.t('topic.change_timestamp.error'), 'alert-error'); self.flash(I18n.t('topic.change_timestamp.error'), 'alert-error');
self.set('saving', false); self.set('saving', false);

View File

@ -2,6 +2,45 @@ import { setting } from 'discourse/lib/computed';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import Quote from 'discourse/lib/quote'; import Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft'; import Draft from 'discourse/models/draft';
import Composer from 'discourse/models/composer';
import computed from 'ember-addons/ember-computed-decorators';
function loadDraft(store, opts) {
opts = opts || {};
let draft = opts.draft;
const draftKey = opts.draftKey;
const draftSequence = opts.draftSequence;
try {
if (draft && typeof draft === 'string') {
draft = JSON.parse(draft);
}
} catch (error) {
draft = null;
Draft.clear(draftKey, draftSequence);
}
if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) {
const composer = store.createRecord('composer');
composer.open({
draftKey,
draftSequence,
action: draft.action,
title: draft.title,
categoryId: draft.categoryId || opts.categoryId,
postId: draft.postId,
archetypeId: draft.archetypeId,
reply: draft.reply,
metaData: draft.metaData,
usernames: draft.usernames,
draft: true,
composerState: Composer.DRAFT,
composerTime: draft.composerTime,
typingTime: draft.typingTime
});
return composer;
}
}
export default Ember.Controller.extend({ export default Ember.Controller.extend({
needs: ['modal', 'topic', 'composer-messages', 'application'], needs: ['modal', 'topic', 'composer-messages', 'application'],
@ -26,6 +65,12 @@ export default Ember.Controller.extend({
this.set('similarTopics', []); this.set('similarTopics', []);
}.on('init'), }.on('init'),
@computed('model.action')
canWhisper(action) {
const currentUser = this.currentUser;
return currentUser && currentUser.get('staff') && this.siteSettings.enable_whispers && action === Composer.REPLY;
},
showWarning: function() { showWarning: function() {
if (!Discourse.User.currentProp('staff')) { return false; } if (!Discourse.User.currentProp('staff')) { return false; }
@ -94,7 +139,6 @@ export default Ember.Controller.extend({
}, },
hitEsc() { hitEsc() {
const messages = this.get('controllers.composer-messages.model'); const messages = this.get('controllers.composer-messages.model');
if (messages.length) { if (messages.length) {
messages.popObject(); messages.popObject();
@ -438,7 +482,7 @@ export default Ember.Controller.extend({
// Given a potential instance and options, set the model for this composer. // Given a potential instance and options, set the model for this composer.
_setModel(composerModel, opts) { _setModel(composerModel, opts) {
if (opts.draft) { if (opts.draft) {
composerModel = Discourse.Composer.loadDraft(opts); composerModel = loadDraft(this.store, opts);
if (composerModel) { if (composerModel) {
composerModel.set('topic', opts.topic); composerModel.set('topic', opts.topic);
} }

View File

@ -6,6 +6,9 @@ export default DiscoveryController.extend({
withLogo: Em.computed.filterBy('model.categories', 'logo_url'), withLogo: Em.computed.filterBy('model.categories', 'logo_url'),
showPostsColumn: Em.computed.empty('withLogo'), showPostsColumn: Em.computed.empty('withLogo'),
// this makes sure the composer isn't scoping to a specific category
category: null,
actions: { actions: {
refresh() { refresh() {
@ -19,8 +22,8 @@ export default DiscoveryController.extend({
this.set('controllers.discovery.loading', true); this.set('controllers.discovery.loading', true);
const parentCategory = this.get('model.parentCategory'); const parentCategory = this.get('model.parentCategory');
const promise = parentCategory ? Discourse.CategoryList.listForParent(parentCategory) : const promise = parentCategory ? Discourse.CategoryList.listForParent(this.store, parentCategory) :
Discourse.CategoryList.list(); Discourse.CategoryList.list(this.store);
const self = this; const self = this;
promise.then(function(list) { promise.then(function(list) {

View File

@ -29,8 +29,8 @@ const controllerOpts = {
}, },
// Show newly inserted topics // Show newly inserted topics
showInserted: function() { showInserted() {
const tracker = Discourse.TopicTrackingState.current(); const tracker = this.topicTrackingState;
// Move inserted into topics // Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming')); this.get('content').loadBefore(tracker.get('newIncoming'));
@ -38,9 +38,8 @@ const controllerOpts = {
return false; return false;
}, },
refresh: function() { refresh() {
const filter = this.get('model.filter'), const filter = this.get('model.filter');
self = this;
this.setProperties({ order: 'default', ascending: false }); this.setProperties({ order: 'default', ascending: false });
@ -52,36 +51,27 @@ const controllerOpts = {
// Lesson learned: Don't call `loading` yourself. // Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true); this.set('controllers.discovery.loading', true);
this.store.findFiltered('topicList', {filter}).then(function(list) { this.store.findFiltered('topicList', {filter}).then((list) => {
Discourse.TopicList.hideUniformCategory(list, self.get('category')); Discourse.TopicList.hideUniformCategory(list, this.get('category'));
self.setProperties({ model: list }); this.setProperties({ model: list });
self.resetSelected(); this.resetSelected();
const tracking = Discourse.TopicTrackingState.current(); if (this.topicTrackingState) {
if (tracking) { this.topicTrackingState.sync(list, filter);
tracking.sync(list, filter);
} }
self.send('loadingComplete'); this.send('loadingComplete');
}); });
}, },
resetNew: function() { resetNew() {
const self = this; this.topicTrackingState.resetNew();
Discourse.Topic.resetNew().then(() => this.send('refresh'));
Discourse.TopicTrackingState.current().resetNew();
Discourse.Topic.resetNew().then(function() {
self.send('refresh');
});
} }
}, },
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),
isFilterPage: function(filter, filterType) { isFilterPage: function(filter, filterType) {
if (!filter) { return false; } if (!filter) { return false; }
return filter.match(new RegExp(filterType + '$', 'gi')) ? true : false; return filter.match(new RegExp(filterType + '$', 'gi')) ? true : false;
@ -95,10 +85,6 @@ const controllerOpts = {
return this.get('model.filter') === 'new' && this.get('model.topics.length') > 0; return this.get('model.filter') === 'new' && this.get('model.topics.length') > 0;
}.property('model.filter', 'model.topics.length'), }.property('model.filter', 'model.topics.length'),
tooManyTracked: function(){
return Discourse.TopicTrackingState.current().tooManyTracked();
}.property(),
showDismissAtTop: function() { showDismissAtTop: function() {
return (this.isFilterPage(this.get('model.filter'), 'new') || return (this.isFilterPage(this.get('model.filter'), 'new') ||
this.isFilterPage(this.get('model.filter'), 'unread')) && this.isFilterPage(this.get('model.filter'), 'unread')) &&

View File

@ -1,3 +1,4 @@
import { observes } from "ember-addons/ember-computed-decorators";
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ModalFunctionality from 'discourse/mixins/modal-functionality';
// Modal related to auto closing of topics // Modal related to auto closing of topics
@ -5,31 +6,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
auto_close_valid: true, auto_close_valid: true,
auto_close_invalid: Em.computed.not('auto_close_valid'), auto_close_invalid: Em.computed.not('auto_close_valid'),
setAutoCloseTime: function() { @observes("model.details.auto_close_at", "model.details.auto_close_hours")
var autoCloseTime = null; setAutoCloseTime() {
let autoCloseTime = null;
if (this.get("model.details.auto_close_based_on_last_post")) { if (this.get("model.details.auto_close_based_on_last_post")) {
autoCloseTime = this.get("model.details.auto_close_hours"); autoCloseTime = this.get("model.details.auto_close_hours");
} else if (this.get("model.details.auto_close_at")) { } else if (this.get("model.details.auto_close_at")) {
var closeTime = new Date(this.get("model.details.auto_close_at")); const closeTime = new Date(this.get("model.details.auto_close_at"));
if (closeTime > new Date()) { if (closeTime > new Date()) {
autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm"); autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm");
} }
} }
this.set("model.auto_close_time", autoCloseTime); this.set("model.auto_close_time", autoCloseTime);
}.observes("model.details.{auto_close_at,auto_close_hours}"),
actions: {
saveAutoClose: function() { this.setAutoClose(this.get("model.auto_close_time")); },
removeAutoClose: function() { this.setAutoClose(null); }
}, },
setAutoClose: function(time) { actions: {
var self = this; saveAutoClose() { this.setAutoClose(this.get("model.auto_close_time")); },
removeAutoClose() { this.setAutoClose(null); }
},
setAutoClose(time) {
const self = this;
this.send('hideModal'); this.send('hideModal');
Discourse.ajax({ Discourse.ajax({
url: '/t/' + this.get('model.id') + '/autoclose', url: `/t/${this.get('model.id')}/autoclose`,
type: 'PUT', type: 'PUT',
dataType: 'json', dataType: 'json',
data: { data: {
@ -37,15 +39,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"), auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"),
timezone_offset: (new Date().getTimezoneOffset()) timezone_offset: (new Date().getTimezoneOffset())
} }
}).then(function(result){ }).then(result => {
if (result.success) { if (result.success) {
self.send('closeModal'); this.send('closeModal');
self.set('model.details.auto_close_at', result.auto_close_at); this.set('model.details.auto_close_at', result.auto_close_at);
self.set('model.details.auto_close_hours', result.auto_close_hours); this.set('model.details.auto_close_hours', result.auto_close_hours);
} else { } else {
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } );
} }
}, function () { }).catch(() => {
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } );
}); });
} }

View File

@ -1,45 +1,125 @@
import { translateResults } from "discourse/lib/search-for-term"; import { translateResults, searchContextDescription, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
import showModal from 'discourse/lib/show-modal';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import Category from 'discourse/models/category';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
needs: ["application"], needs: ["application"],
loading: Em.computed.not("model"), loading: Em.computed.not("model"),
queryParams: ["q"], queryParams: ["q", "context_id", "context", "skip_context"],
q: null, q: null,
selected: [], selected: [],
context_id: null,
context: null,
modelChanged: function() { @computed('q')
hasAutofocus(q) {
return Em.isEmpty(q);
},
@computed('skip_context', 'context')
searchContextEnabled: {
get(skip,context){
return (!skip && context) || skip === "false";
},
set(val) {
this.set('skip_context', val ? "false" : "true" )
}
},
@computed('context', 'context_id')
searchContextDescription(context, id){
var name = id;
if (context === 'category') {
var category = Category.findById(id);
if (!category) {return;}
name = category.get('name');
}
return searchContextDescription(context, name);
},
@computed('q')
searchActive(q){
return isValidSearchTerm(q);
},
@computed('searchTerm')
isNotValidSearchTerm(searchTerm) {
return !isValidSearchTerm(searchTerm);
},
@observes('model')
modelChanged() {
if (this.get("searchTerm") !== this.get("q")) { if (this.get("searchTerm") !== this.get("q")) {
this.set("searchTerm", this.get("q")); this.set("searchTerm", this.get("q"));
} }
}.observes("model"), },
qChanged: function() { @observes('q')
qChanged() {
const model = this.get("model"); const model = this.get("model");
if (model && this.get("model.q") !== this.get("q")) { if (model && this.get("model.q") !== this.get("q")) {
this.set("searchTerm", this.get("q")); this.set("searchTerm", this.get("q"));
this.send("search"); this.send("search");
} }
}.observes("q"), },
_showFooter: function() { @observes('loading')
_showFooter() {
this.set("controllers.application.showFooter", !this.get("loading")); this.set("controllers.application.showFooter", !this.get("loading"));
}.observes("loading"), },
canBulkSelect: Em.computed.alias('currentUser.staff'), canBulkSelect: Em.computed.alias('currentUser.staff'),
search(){ search(){
if (this._searching) {
return;
}
this._searching = true;
const router = Discourse.__container__.lookup('router:main');
this.set("q", this.get("searchTerm")); this.set("q", this.get("searchTerm"));
this.set("model", null); this.set("model", null);
Discourse.ajax("/search", { data: { q: this.get("searchTerm") } }).then(results => { var args = { q: this.get("searchTerm") };
this.set("model", translateResults(results) || {});
this.set("model.q", this.get("q")); const skip = this.get("skip_context");
}); if ((!skip && this.get('context')) || skip==="false"){
args.search_context = {
type: this.get('context'),
id: this.get('context_id')
};
}
const searchKey = getSearchKey(args);
Discourse.ajax("/search", { data: args }).then(results => {
const model = translateResults(results) || {};
router.transientCache('lastSearch', { searchKey, model }, 5);
this.set("model", model);
}).finally(() => {this._searching = false});
}, },
actions: { actions: {
selectAll() {
this.get('selected').addObjects(this.get('model.posts').map(r => r.topic));
// Doing this the proper way is a HUGE pain,
// we can hack this to work by observing each on the array
// in the component, however, when we select ANYTHING, we would force
// 50 traversals of the list
// This hack is cheap and easy
$('.fps-result input[type=checkbox]').prop('checked', true);
},
clearAll() {
this.get('selected').clear()
$('.fps-result input[type=checkbox]').prop('checked', false);
},
toggleBulkSelect() { toggleBulkSelect() {
this.toggleProperty('bulkSelectEnabled'); this.toggleProperty('bulkSelectEnabled');
this.get('selected').clear(); this.get('selected').clear();
@ -51,7 +131,15 @@ export default Ember.Controller.extend({
this.search(); this.search();
}, },
showSearchHelp() {
// TODO: dupe code should be centralized
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
showModal('searchHelp', { model });
});
},
search() { search() {
if (this.get("isNotValidSearchTerm")) return;
this.search(); this.search();
} }
} }

View File

@ -1,3 +1,5 @@
import DiscourseURL from 'discourse/lib/url';
const HeaderController = Ember.Controller.extend({ const HeaderController = Ember.Controller.extend({
topic: null, topic: null,
showExtraInfo: null, showExtraInfo: null,
@ -18,6 +20,24 @@ const HeaderController = Ember.Controller.extend({
actions: { actions: {
showUserMenu() {
if (!this.get('userMenuVisible')) {
this.appEvents.trigger('dropdowns:closeAll');
this.set('userMenuVisible', true);
}
},
fullPageSearch() {
const searchService = this.container.lookup('search-service:main');
const context = searchService.get('searchContext');
var params = "";
if (context) {
params = `?context=${context.type}&context_id=${context.id}`;
}
DiscourseURL.routeTo('/search' + params);
},
toggleMenuPanel(visibleProp) { toggleMenuPanel(visibleProp) {
this.toggleProperty(visibleProp); this.toggleProperty(visibleProp);
this.appEvents.trigger('dropdowns:closeAll'); this.appEvents.trigger('dropdowns:closeAll');

View File

@ -1,3 +1,4 @@
import computed from "ember-addons/ember-computed-decorators";
import NavigationDefaultController from 'discourse/controllers/navigation/default'; import NavigationDefaultController from 'discourse/controllers/navigation/default';
import { setting } from 'discourse/lib/computed'; import { setting } from 'discourse/lib/computed';
@ -6,8 +7,9 @@ export default NavigationDefaultController.extend({
showingParentCategory: Em.computed.none('category.parentCategory'), showingParentCategory: Em.computed.none('category.parentCategory'),
showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'), showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'),
navItems: function() { @computed("showingSubcategoryList", "category", "noSubcategories")
if (this.get('showingSubcategoryList')) { return []; } navItems(showingSubcategoryList, category, noSubcategories) {
return Discourse.NavItem.buildList(this.get('category'), { noSubcategories: this.get('noSubcategories') }); if (showingSubcategoryList) { return []; }
}.property('category', 'noSubcategories') return Discourse.NavItem.buildList(category, { noSubcategories });
}
}); });

View File

@ -1,12 +1,18 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({ export default Ember.Controller.extend({
needs: ['discovery', 'discovery/topics'], needs: ['discovery', 'discovery/topics'],
categories: function() { @computed()
categories() {
return Discourse.Category.list(); return Discourse.Category.list();
}.property(), },
navItems: function() { @computed("filterMode")
return Discourse.NavItem.buildList(null, {filterMode: this.get('filterMode')}); navItems(filterMode) {
}.property('filterMode') // we don't want to show the period in the navigation bar since it's in a dropdown
if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
return Discourse.NavItem.buildList(null, { filterMode });
}
}); });

View File

@ -1,6 +1,7 @@
import { setting } from 'discourse/lib/computed'; import { setting } from 'discourse/lib/computed';
import CanCheckEmails from 'discourse/mixins/can-check-emails'; import CanCheckEmails from 'discourse/mixins/can-check-emails';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(CanCheckEmails, { export default Ember.Controller.extend(CanCheckEmails, {
@ -10,18 +11,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
allowBackgrounds: setting('allow_profile_backgrounds'), allowBackgrounds: setting('allow_profile_backgrounds'),
editHistoryVisible: setting('edit_history_visible_to_public'), editHistoryVisible: setting('edit_history_visible_to_public'),
selectedCategories: function(){ @computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
return [].concat(this.get("model.watchedCategories"), selectedCategories(watched, tracked, muted) {
this.get("model.trackedCategories"), return [].concat(watched, tracked, muted);
this.get("model.mutedCategories")); },
}.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"),
// By default we haven't saved anything // By default we haven't saved anything
saved: false, saved: false,
newNameInput: null, newNameInput: null,
userFields: function() { @computed("model.user_fields.@each.value")
userFields() {
let siteUserFields = this.site.get('user_fields'); let siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) { if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('model.user_fields'); const userFields = this.get('model.user_fields');
@ -35,34 +36,37 @@ export default Ember.Controller.extend(CanCheckEmails, {
return Ember.Object.create({ value, field }); return Ember.Object.create({ value, field });
}); });
} }
}.property('model.user_fields.@each.value'), },
cannotDeleteAccount: Em.computed.not('can_delete_account'), cannotDeleteAccount: Em.computed.not('can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
canEditName: setting('enable_names'), canEditName: setting('enable_names'),
nameInstructions: function() { @computed()
nameInstructions() {
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
}.property(), },
canSelectTitle: function() { @computed("model.has_title_badges")
return this.siteSettings.enable_badges && this.get('model.has_title_badges'); canSelectTitle(hasTitleBadges) {
}.property('model.badge_count'), return this.siteSettings.enable_badges && hasTitleBadges;
},
canChangePassword: function() { @computed()
canChangePassword() {
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins; return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
}.property(), },
canReceiveDigest: function() { @computed()
canReceiveDigest() {
return !this.siteSettings.disable_digest_emails; return !this.siteSettings.disable_digest_emails;
}.property(), },
availableLocales: function() { @computed()
return this.siteSettings.available_locales.split('|').map( function(s) { availableLocales() {
return {name: s, value: s}; return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s }));
}); },
}.property(),
digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 }, 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.every_three_days'), value: 3 },
@ -86,16 +90,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
{ name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 }, { name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 },
{ name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }],
saveButtonText: function() { @computed("model.isSaving")
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save'); saveButtonText(isSaving) {
}.property('model.isSaving'), return isSaving ? I18n.t('saving') : I18n.t('save');
},
passwordProgress: null, passwordProgress: null,
actions: { actions: {
save() { save() {
const self = this;
this.set('saved', false); this.set('saved', false);
const model = this.get('model'); const model = this.get('model');
@ -113,28 +117,27 @@ export default Ember.Controller.extend(CanCheckEmails, {
// Cook the bio for preview // Cook the bio for preview
model.set('name', this.get('newNameInput')); model.set('name', this.get('newNameInput'));
return model.save().then(function() { return model.save().then(() => {
if (Discourse.User.currentProp('id') === model.get('id')) { if (Discourse.User.currentProp('id') === model.get('id')) {
Discourse.User.currentProp('name', model.get('name')); Discourse.User.currentProp('name', model.get('name'));
} }
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw')))); model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
self.set('saved', true); this.set('saved', true);
}).catch(popupAjaxError); }).catch(popupAjaxError);
}, },
changePassword() { changePassword() {
const self = this;
if (!this.get('passwordProgress')) { if (!this.get('passwordProgress')) {
this.set('passwordProgress', I18n.t("user.change_password.in_progress")); this.set('passwordProgress', I18n.t("user.change_password.in_progress"));
return this.get('model').changePassword().then(function() { return this.get('model').changePassword().then(() => {
// password changed // password changed
self.setProperties({ this.setProperties({
changePasswordProgress: false, changePasswordProgress: false,
passwordProgress: I18n.t("user.change_password.success") passwordProgress: I18n.t("user.change_password.success")
}); });
}, function() { }).catch(() => {
// password failed to change // password failed to change
self.setProperties({ this.setProperties({
changePasswordProgress: false, changePasswordProgress: false,
passwordProgress: I18n.t("user.change_password.error") passwordProgress: I18n.t("user.change_password.error")
}); });

View File

@ -0,0 +1,94 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
import Ember from 'ember';
const SortableArrayProxy = Ember.ArrayProxy.extend(Ember.SortableMixin);
export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
@computed("site.categories")
categoriesBuffered(categories) {
const bufProxy = Ember.ObjectProxy.extend(BufferedProxy);
return categories.map(c => bufProxy.create({ content: c }));
},
categoriesOrdered: function() {
return SortableArrayProxy.create({
sortProperties: ['content.position'],
content: this.get('categoriesBuffered')
});
}.property('categoriesBuffered'),
showFixIndices: function() {
const cats = this.get('categoriesOrdered');
const len = cats.get('length');
for (let i = 0; i < len; i++) {
if (cats.objectAt(i).get('position') !== i) {
return true;
}
}
return false;
}.property('categoriesOrdered.@each.position'),
showApplyAll: function() {
let anyChanged = false;
this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges') });
return anyChanged;
}.property('categoriesBuffered.@each.hasBufferedChanges'),
saveDisabled: Ember.computed.or('showApplyAll', 'showFixIndices'),
moveDir(cat, dir) {
const cats = this.get('categoriesOrdered');
const curIdx = cats.indexOf(cat);
const desiredIdx = curIdx + dir;
if (desiredIdx >= 0 && desiredIdx < cats.get('length')) {
const curPos = cat.get('position');
cat.set('position', curPos + dir);
const otherCat = cats.objectAt(desiredIdx);
otherCat.set('position', curPos - dir);
this.send('commit');
}
},
actions: {
moveUp(cat) {
this.moveDir(cat, -1);
},
moveDown(cat) {
this.moveDir(cat, 1);
},
fixIndices() {
const cats = this.get('categoriesOrdered');
const len = cats.get('length');
for (let i = 0; i < len; i++) {
cats.objectAt(i).set('position', i);
}
this.send('commit');
},
commit() {
this.get('categoriesBuffered').forEach(bc => {
if (bc.get('hasBufferedChanges')) {
bc.applyBufferedChanges();
}
});
this.propertyDidChange('categoriesBuffered');
},
saveOrder() {
const data = {};
this.get('categoriesBuffered').forEach((cat) => {
data[cat.get('id')] = cat.get('position');
});
Discourse.ajax('/categories/reorder',
{type: 'POST', data: {mapping: JSON.stringify(data)}}).
then(() => this.send("closeModal")).
catch(popupAjaxError);
}
}
});

View File

@ -3,8 +3,11 @@ export default Ember.Controller.extend({
actions: { actions: {
markFaqRead() { markFaqRead() {
if (this.currentUser) { const currentUser = this.currentUser;
Discourse.ajax("/users/read-faq", { method: "POST" }); if (currentUser) {
Discourse.ajax("/users/read-faq", { method: "POST" }).then(() => {
currentUser.set('read_faq', true);
});
} }
} }
} }

View File

@ -1,7 +1,7 @@
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
function entranceDate(dt, showTime) { function entranceDate(dt, showTime) {
var today = new Date(); const today = new Date();
if (dt.toDateString() === today.toDateString()) { if (dt.toDateString() === today.toDateString()) {
return moment(dt).format(I18n.t("dates.time")); return moment(dt).format(I18n.t("dates.time"));
@ -44,7 +44,7 @@ export default Ember.Controller.extend({
}.property('bumpedDate'), }.property('bumpedDate'),
actions: { actions: {
show: function(data) { show(data) {
// Show the chooser but only if the model changes // Show the chooser but only if the model changes
if (this.get('model') !== data.topic) { if (this.get('model') !== data.topic) {
this.set('model', data.topic); this.set('model', data.topic);
@ -52,11 +52,11 @@ export default Ember.Controller.extend({
} }
}, },
enterTop: function() { enterTop() {
DiscourseURL.routeTo(this.get('model.url')); DiscourseURL.routeTo(this.get('model.url'));
}, },
enterBottom: function() { enterBottom() {
DiscourseURL.routeTo(this.get('model.lastPostUrl')); DiscourseURL.routeTo(this.get('model.lastPostUrl'));
} }
} }

View File

@ -428,20 +428,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}, },
toggleWiki(post) { toggleWiki(post) {
// the request to the server is made in an observer in the post class post.updatePostField('wiki', !post.get('wiki'));
post.toggleProperty('wiki');
}, },
togglePostType(post) { togglePostType(post) {
// the request to the server is made in an observer in the post class const regular = this.site.get('post_types.regular');
const regular = this.site.get('post_types.regular'), const moderator = this.site.get('post_types.moderator_action');
moderator = this.site.get('post_types.moderator_action');
if (post.get("post_type") === moderator) { post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
post.set("post_type", regular);
} else {
post.set("post_type", moderator);
}
}, },
rebakePost(post) { rebakePost(post) {

View File

@ -37,7 +37,7 @@ export default Ember.Controller.extend({
show(username, postId, target) { show(username, postId, target) {
// XSS protection (should be encapsulated) // XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_]/g, ""); username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
// Don't show on mobile // Don't show on mobile
if (Discourse.Mobile.mobileView) { if (Discourse.Mobile.mobileView) {

View File

@ -1,5 +1,6 @@
import { exportUserArchive } from 'discourse/lib/export-csv'; import { exportUserArchive } from 'discourse/lib/export-csv';
import CanCheckEmails from 'discourse/mixins/can-check-emails'; import CanCheckEmails from 'discourse/mixins/can-check-emails';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend(CanCheckEmails, { export default Ember.Controller.extend(CanCheckEmails, {
indexStream: false, indexStream: false,
@ -11,7 +12,10 @@ export default Ember.Controller.extend(CanCheckEmails, {
return this.get('content.username') === Discourse.User.currentProp('username'); return this.get('content.username') === Discourse.User.currentProp('username');
}.property('content.username'), }.property('content.username'),
collapsedInfo: Em.computed.not('indexStream'), @computed('indexStream', 'viewingSelf', 'forceExpand')
collapsedInfo(indexStream, viewingSelf, forceExpand){
return (!indexStream || viewingSelf) && !forceExpand;
},
linkWebsite: Em.computed.not('model.isBasic'), linkWebsite: Em.computed.not('model.isBasic'),
@ -19,7 +23,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
return this.get('model.trust_level') > 2 && !this.siteSettings.tl3_links_no_follow; return this.get('model.trust_level') > 2 && !this.siteSettings.tl3_links_no_follow;
}.property('model.trust_level'), }.property('model.trust_level'),
canSeePrivateMessages: Ember.computed.or('viewingSelf', 'currentUser.admin'), @computed('viewSelf', 'currentUser.admin')
canSeePrivateMessages(viewingSelf, isAdmin) {
return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin);
},
canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'), canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'),
showBadges: function() { showBadges: function() {
@ -59,6 +67,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'), privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'),
actions: { actions: {
expandProfile: function() {
this.set('forceExpand', true);
},
adminDelete: function() { adminDelete: function() {
Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){ Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){
user.destroy({deletePosts: true}); user.destroy({deletePosts: true});

View File

@ -33,6 +33,7 @@ function codeFlattenBlocks(blocks) {
Discourse.Dialect.replaceBlock({ Discourse.Dialect.replaceBlock({
start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm, start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
stop: /^```$/gm, stop: /^```$/gm,
withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match
emitter: function(blockContents, matches) { emitter: function(blockContents, matches) {
var klass = Discourse.SiteSettings.default_code_lang; var klass = Discourse.SiteSettings.default_code_lang;

View File

@ -501,6 +501,12 @@ Discourse.Dialect = {
var pos = args.start.lastIndex - match[0].length, var pos = args.start.lastIndex - match[0].length,
leading = block.slice(0, pos), leading = block.slice(0, pos),
trailing = match[2] ? match[2].replace(/^\n*/, "") : ""; trailing = match[2] ? match[2].replace(/^\n*/, "") : "";
if(args.withoutLeading && args.withoutLeading.test(leading)) {
//The other leading block should be processed first! eg a code block wrapped around a code block.
return;
}
// just give up if there's no stop tag in this or any next block // just give up if there's no stop tag in this or any next block
args.stop.lastIndex = block.length - trailing.length; args.stop.lastIndex = block.length - trailing.length;
if (!args.stop.exec(block) && lastChance()) { return; } if (!args.stop.exec(block) && lastChance()) { return; }

View File

@ -7,7 +7,7 @@ Discourse.Dialect.inlineRegexp({
start: '@', start: '@',
// NOTE: we really should be using SiteSettings here, but it loads later in process // NOTE: we really should be using SiteSettings here, but it loads later in process
// also, if we do, we must ensure serverside version works as well // also, if we do, we must ensure serverside version works as well
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_]{0,40})/, matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9])/,
wordBoundary: true, wordBoundary: true,
emitter: function(matches) { emitter: function(matches) {

View File

@ -1,37 +1,29 @@
import registerUnbound from 'discourse/helpers/register-unbound'; import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter'; import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter';
const safe = Handlebars.SafeString; const safe = Handlebars.SafeString;
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { Em.Handlebars.helper('bound-avatar', (user, size) => {
if (Em.isEmpty(user)) { if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>"); return new safe("<div class='avatar-placeholder'></div>");
} }
const username = Em.get(user, 'username'); const avatar = Em.get(user, 'avatar_template');
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId);
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));
}, 'username', 'uploaded_avatar_id', 'avatar_template'); }, 'username', 'avatar_template');
/* /*
* Used when we only have a template * Used when we only have a template
*/ */
Em.Handlebars.helper('bound-avatar-template', function(at, size) { Em.Handlebars.helper('bound-avatar-template', (at, size) => {
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at })); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at }));
}); });
registerUnbound('raw-date', function(dt) { registerUnbound('raw-date', dt => longDate(new Date(dt)));
return longDate(new Date(dt));
});
registerUnbound('age-with-tooltip', function(dt) { registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}));
});
registerUnbound('number', function(orig, params) { registerUnbound('number', (orig, params) => {
orig = parseInt(orig, 10); orig = parseInt(orig, 10);
if (isNaN(orig)) { orig = 0; } if (isNaN(orig)) { orig = 0; }

View File

@ -47,6 +47,13 @@
**/ **/
// TODO: Add all plugin-outlet names dynamically
const rewireableOutlets = [
'hamburger-admin'
];
const _rewires = {};
let _connectorCache, _rawCache; let _connectorCache, _rawCache;
function findOutlets(collection, callback) { function findOutlets(collection, callback) {
@ -63,9 +70,17 @@ function findOutlets(collection, callback) {
} }
const segments = res.split("/"); const segments = res.split("/");
const outletName = segments[segments.length-2]; let outletName = segments[segments.length-2];
const uniqueName = segments[segments.length-1]; const uniqueName = segments[segments.length-1];
const outletRewires = _rewires[outletName];
if (outletRewires) {
const newOutlet = outletRewires[uniqueName];
if (newOutlet) {
outletName = newOutlet;
}
}
const dashedName = outletName.replace(/_/g, '-'); const dashedName = outletName.replace(/_/g, '-');
if (dashedName !== outletName) { if (dashedName !== outletName) {
Ember.warn("DEPRECATION: You need to use dashes in outlet names, not underscores"); Ember.warn("DEPRECATION: You need to use dashes in outlet names, not underscores");
@ -179,4 +194,11 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options,
} }
}); });
// Allow plugins to rewire outlets to new outlets if they exist. For example, the akismet
// plugin will use `hamburger-admin` if it exists, otherwise `site-menu-links`
export function rewire(uniqueName, outlet, wantedOutlet) {
if (rewireableOutlets.indexOf(wantedOutlet) !== -1) {
_rewires[outlet] = _rewires[outlet] || {};
_rewires[outlet][uniqueName] = wantedOutlet;
}
}

View File

@ -1,24 +1,23 @@
import registerUnbound from 'discourse/helpers/register-unbound'; import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
function renderAvatar(user, options) { function renderAvatar(user, options) {
options = options || {}; options = options || {};
if (user) { if (user) {
var username = Em.get(user, 'username');
if (!username) {
if (!options.usernamePath) { return ''; }
username = Em.get(user, options.usernamePath);
}
var title; const username = Em.get(user, options.usernamePath || 'username');
const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template');
if (!username || !avatarTemplate) { return ''; }
let title;
if (!options.ignoreTitle) { if (!options.ignoreTitle) {
// first try to get a title // first try to get a title
title = Em.get(user, 'title'); title = Em.get(user, 'title');
// if there was no title provided // if there was no title provided
if (!title) { if (!title) {
// try to retrieve a description // try to retrieve a description
var description = Em.get(user, 'description'); const description = Em.get(user, 'description');
// if a description has been provided // if a description has been provided
if (description && description.length > 0) { if (description && description.length > 0) {
// preprend the username before the description // preprend the username before the description
@ -27,14 +26,11 @@ function renderAvatar(user, options) {
} }
} }
// this is simply done to ensure we cache images correctly
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
return Discourse.Utilities.avatarImg({ return Discourse.Utilities.avatarImg({
size: options.imageSize, size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses, extraClasses: Em.get(user, 'extras') || options.extraClasses,
title: title || username, title: title || username,
avatarTemplate: avatarTemplate(username, uploadedAvatarId) avatarTemplate: avatarTemplate
}); });
} else { } else {
return ''; return '';

View File

@ -1,37 +1,10 @@
import interceptClick from 'discourse/lib/intercept-click';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
/**
Discourse does some server side rendering of HTML, such as the `cooked` contents of
posts. The downside of this in an Ember app is the links will not go through the router.
This jQuery code intercepts clicks on those links and routes them properly.
**/
export default { export default {
name: "click-interceptor", name: "click-interceptor",
initialize: function() { initialize() {
$('#main').on('click.discourse', 'a', function(e) { $('#main').on('click.discourse', 'a', interceptClick);
if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } $(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash));
var $currentTarget = $(e.currentTarget),
href = $currentTarget.attr('href');
if (!href ||
href === '#' ||
$currentTarget.attr('target') ||
$currentTarget.data('ember-action') ||
$currentTarget.data('auto-route') ||
$currentTarget.data('share-url') ||
$currentTarget.data('user-card') ||
$currentTarget.hasClass('mention') ||
$currentTarget.hasClass('ember-view') ||
$currentTarget.hasClass('lightbox') ||
href.indexOf("mailto:") === 0 ||
(href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) {
return;
}
e.preventDefault();
DiscourseURL.routeTo(href);
return false;
});
} }
}; };

View File

@ -6,6 +6,9 @@ export default {
initialize(container) { initialize(container) {
const cache = {};
var transitionCount = 0;
// Tell our AJAX system to track a page transition // Tell our AJAX system to track a page transition
const router = container.lookup('router:main'); const router = container.lookup('router:main');
router.on('willTransition', function() { router.on('willTransition', function() {
@ -14,7 +17,22 @@ export default {
router.on('didTransition', function() { router.on('didTransition', function() {
Em.run.scheduleOnce('afterRender', Ember.Route, cleanDOM); Em.run.scheduleOnce('afterRender', Ember.Route, cleanDOM);
transitionCount++;
_.each(cache, (v,k) => {
if (v && v.target && v.target < transitionCount) {
delete cache[k];
}
}); });
});
router.transientCache = function(key, data, count) {
if (data === undefined) {
return cache[key];
} else {
return cache[key] = {data, target: transitionCount + count};
}
};
const pageTracker = PageTracker.current(); const pageTracker = PageTracker.current();
pageTracker.start(); pageTracker.start();

View File

@ -8,7 +8,12 @@ export default {
const user = container.lookup('current-user:main'), const user = container.lookup('current-user:main'),
site = container.lookup('site:main'), site = container.lookup('site:main'),
siteSettings = container.lookup('site-settings:main'), siteSettings = container.lookup('site-settings:main'),
bus = container.lookup('message-bus:main'); bus = container.lookup('message-bus:main'),
keyValueStore = container.lookup('key-value-store:main');
// clear old cached notifications
// they could be a week old for all we know
keyValueStore.remove('recent-notifications');
if (user) { if (user) {
@ -38,6 +43,32 @@ export default {
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
user.set('lastNotificationChange', new Date()); user.set('lastNotificationChange', new Date());
} }
var stale = keyValueStore.getObject('recent-notifications');
const lastNotification = data.last_notification && data.last_notification.notification;
if (stale && stale.notifications && lastNotification) {
const oldNotifications = stale.notifications;
const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id});
if (staleIndex > -1) {
oldNotifications.splice(staleIndex, 1);
}
// this gets a bit tricky, uread pms are bumped to front
var insertPosition = 0;
if (lastNotification.notification_type !== 6) {
insertPosition = _.findIndex(oldNotifications, function(n){
return n.notification_type !== 6 || n.read;
});
insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition;
}
oldNotifications.splice(insertPosition, 0, lastNotification);
keyValueStore.setItem('recent-notifications', JSON.stringify(stale));
}
}, user.notification_channel_position); }, user.notification_channel_position);
bus.subscribe("/categories", function(data) { bus.subscribe("/categories", function(data) {

View File

@ -91,7 +91,7 @@ export default function(options) {
transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item]; transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item];
var divs = transformed.map(function(itm) { var divs = transformed.map(function(itm) {
var d = $("<div class='item'><span>" + itm + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>"); var d = $("<div class='item'><span>" + itm + "<a class='remove' href><i class='fa fa-times'></i></a></span></div>");
var prev = me.parent().find('.item:last'); var prev = me.parent().find('.item:last');
if (prev.length === 0) { if (prev.length === 0) {
me.parent().prepend(d); me.parent().prepend(d);
@ -220,6 +220,13 @@ export default function(options) {
vOffset = div.height(); vOffset = div.height();
} }
if (Discourse.Mobile.mobileView && !isInput) {
div.css('width', 'auto');
if ((me.height() / 2) >= pos.top) { vOffset = -23; }
if ((me.width() / 2) <= pos.left) { hOffset = -div.width(); }
}
var mePos = me.position(); var mePos = me.position();
var borderTop = parseInt(me.css('border-top-width'), 10) || 0; var borderTop = parseInt(me.css('border-top-width'), 10) || 0;
div.css({ div.css({

View File

@ -1,32 +0,0 @@
import { hashString } from 'discourse/lib/hash';
let _splitAvatars;
function defaultAvatar(username) {
const defaultAvatars = Discourse.SiteSettings.default_avatars;
if (defaultAvatars && defaultAvatars.length) {
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
if (_splitAvatars.length) {
const hash = hashString(username);
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
}
}
return Discourse.getURLWithCDN("/letter_avatar/" +
username.toLowerCase() +
"/{size}/" +
Discourse.LetterAvatarVersion + ".png");
}
export default function(username, uploadedAvatarId) {
if (uploadedAvatarId) {
return Discourse.getURLWithCDN("/user_avatar/" +
Discourse.BaseUrl +
"/" +
username.toLowerCase() +
"/{size}/" +
uploadedAvatarId + ".png");
}
return defaultAvatar(username);
}

View File

@ -0,0 +1,29 @@
// The binarySearch() function is licensed under the UNLICENSE
// https://github.com/Olical/binary-search
// Modified for use in Discourse
export default function binarySearch(list, target, keyProp) {
var min = 0;
var max = list.length - 1;
var guess;
var keyProperty = keyProp || "id";
while (min <= max) {
guess = Math.floor((min + max) / 2);
if (Em.get(list[guess], keyProperty) === target) {
return guess;
}
else {
if (Em.get(list[guess], keyProperty) < target) {
min = guess + 1;
}
else {
max = guess - 1;
}
}
}
return -Math.floor((min + max) / 2);
}

View File

@ -77,7 +77,8 @@ function imageFor(code) {
code = code.toLowerCase(); code = code.toLowerCase();
var url = urlFor(code); var url = urlFor(code);
if (url) { if (url) {
return ['img', { href: url, title: ':' + code + ':', 'class': 'emoji', alt: code }]; var code = ':' + code + ':';
return ['img', { href: url, title: code, 'class': 'emoji', alt: code }];
} }
} }

View File

@ -0,0 +1,32 @@
import DiscourseURL from 'discourse/lib/url';
/**
Discourse does some server side rendering of HTML, such as the `cooked` contents of
posts. The downside of this in an Ember app is the links will not go through the router.
This jQuery code intercepts clicks on those links and routes them properly.
**/
export default function interceptClick(e) {
if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; }
const $currentTarget = $(e.currentTarget),
href = $currentTarget.attr('href');
if (!href ||
href === '#' ||
$currentTarget.attr('target') ||
$currentTarget.data('ember-action') ||
$currentTarget.data('auto-route') ||
$currentTarget.data('share-url') ||
$currentTarget.data('user-card') ||
$currentTarget.hasClass('mention') ||
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
$currentTarget.hasClass('lightbox') ||
href.indexOf("mailto:") === 0 ||
(href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) {
return;
}
e.preventDefault();
DiscourseURL.routeTo(href);
return false;
}

View File

@ -32,6 +32,7 @@ KeyValueStore.prototype = {
}, },
remove(key) { remove(key) {
if (!safeLocalStorage) { return; }
return safeLocalStorage.removeItem(this.context + key); return safeLocalStorage.removeItem(this.context + key);
}, },
@ -51,6 +52,13 @@ KeyValueStore.prototype = {
const result = parseInt(this.get(key)); const result = parseInt(this.get(key));
if (!isFinite(result)) { return def; } if (!isFinite(result)) { return def; }
return result; return result;
},
getObject(key) {
if (!safeLocalStorage) { return null; }
try {
return JSON.parse(safeLocalStorage[this.context + key]);
} catch(e) {}
} }
}; };

View File

@ -1,61 +1,53 @@
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
const PATH_BINDINGS = { const bindings = {
'g h': '/', '!': {postAction: 'showFlags'},
'g l': '/latest', '#': {handler: 'toggleProgress', anonymous: true},
'g n': '/new', '/': {handler: 'showSearch', anonymous: true},
'g u': '/unread', '=': {handler: 'toggleHamburgerMenu', anonymous: true},
'g c': '/categories', '?': {handler: 'showHelpModal', anonymous: true},
'g t': '/top', '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
'g b': '/bookmarks', 'b': {handler: 'toggleBookmark'},
'g p': '/my/activity' 'c': {handler: 'createTopic'},
}, 'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true},
'command+f': {handler: 'showBuiltinSearch', anonymous: true},
SELECTED_POST_BINDINGS = { 'd': {postAction: 'deletePost'},
'd': 'deletePost', 'e': {postAction: 'editPost'},
'e': 'editPost', 'end': {handler: 'goToLastPost', anonymous: true},
'l': 'toggleLike', 'f': {handler: 'toggleBookmarkTopic'},
'r': 'replyToPost', 'g h': {path: '/', anonymous: true},
'!': 'showFlags', 'g l': {path: '/latest', anonymous: true},
't': 'replyAsNewTopic' 'g n': {path: '/new'},
}, 'g u': {path: '/unread'},
'g c': {path: '/categories', anonymous: true},
CLICK_BINDINGS = { 'g t': {path: '/top', anonymous: true},
'm m': 'div.notification-options li[data-id="0"] a', // mark topic as muted 'g b': {path: '/bookmarks'},
'm r': 'div.notification-options li[data-id="1"] a', // mark topic as regular 'g p': {path: '/my/activity'},
'm t': 'div.notification-options li[data-id="2"] a', // mark topic as tracking 'g m': {path: '/my/messages'},
'm w': 'div.notification-options li[data-id="3"] a', // mark topic as watching 'home': {handler: 'goToFirstPost', anonymous: true},
'x r': '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top', // dismiss new/posts 'j': {handler: 'selectDown', anonymous: true},
'x t': '#dismiss-topics,#dismiss-topics-top', // dismiss topics 'k': {handler: 'selectUp', anonymous: true},
'.': '.alert.alert-info.clickable', // show incoming/updated topics 'l': {postAction: 'toggleLike'},
'o,enter': '.topic-list tr.selected a.title', // open selected topic 'm m': {click: 'div.notification-options li[data-id="0"] a'}, // mark topic as muted
'shift+s': '#topic-footer-buttons button.share', // share topic 'm r': {click: 'div.notification-options li[data-id="1"] a'}, // mark topic as regular
's': '.topic-post.selected a.post-date' // share post 'm t': {click: 'div.notification-options li[data-id="2"] a'}, // mark topic as tracking
}, 'm w': {click: 'div.notification-options li[data-id="3"] a'}, // mark topic as watching
'o,enter': {click: '.topic-list tr.selected a.title', anonymous: true}, // open selected topic
FUNCTION_BINDINGS = { 'p': {handler: 'showCurrentUser'},
'c': 'createTopic', // create new topic 'q': {handler: 'quoteReply'},
'home': 'goToFirstPost', 'r': {postAction: 'replyToPost'},
'#': 'toggleProgress', 's': {click: '.topic-post.selected a.post-date', anonymous: true}, // share post
'end': 'goToLastPost', 'shift+j': {handler: 'nextSection', anonymous: true},
'shift+j': 'nextSection', 'shift+k': {handler: 'prevSection', anonymous: true},
'j': 'selectDown', 'shift+p': {handler: 'pinUnpinTopic'},
'shift+k': 'prevSection', 'shift+r': {handler: 'replyToTopic'},
'shift+p': 'pinUnpinTopic', 'shift+s': {click: '#topic-footer-buttons button.share', anonymous: true}, // share topic
'k': 'selectUp', 'shift+z shift+z': {handler: 'logout'},
'u': 'goBack', 't': {postAction: 'replyAsNewTopic'},
'/': 'showSearch', 'u': {handler: 'goBack', anonymous: true},
'=': 'toggleHamburgerMenu', 'x r': {click: '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top'}, // dismiss new/posts
'p': 'showCurrentUser', // open current user menu 'x t': {click: '#dismiss-topics,#dismiss-topics-top'} // dismiss topics
'ctrl+f': 'showBuiltinSearch', };
'command+f': 'showBuiltinSearch',
'?': 'showHelpModal', // open keyboard shortcut help
'q': 'quoteReply',
'b': 'toggleBookmark',
'f': 'toggleBookmarkTopic',
'shift+r': 'replyToTopic',
'shift+z shift+z': 'logout'
};
export default { export default {
@ -64,14 +56,24 @@ export default {
this.container = container; this.container = container;
this._stopCallback(); this._stopCallback();
this.searchService = this.container.lookup('search-service:main'); this.searchService = this.container.lookup('search-service:main');
this.appEvents = this.container.lookup('app-events:main'); this.appEvents = this.container.lookup('app-events:main');
this.currentUser = this.container.lookup('current-user:main');
_.each(PATH_BINDINGS, this._bindToPath, this); Object.keys(bindings).forEach(key => {
_.each(CLICK_BINDINGS, this._bindToClick, this); const binding = bindings[key];
_.each(SELECTED_POST_BINDINGS, this._bindToSelectedPost, this); if (!binding.anonymous && !this.currentUser) { return; }
_.each(FUNCTION_BINDINGS, this._bindToFunction, this);
if (binding.path) {
this._bindToPath(binding.path, key);
} else if (binding.handler) {
this._bindToFunction(binding.handler, key);
} else if (binding.postAction) {
this._bindToSelectedPost(binding.postAction, key);
} else if (binding.click) {
this._bindToClick(binding.click, key);
}
});
}, },
toggleBookmark() { toggleBookmark() {
@ -222,17 +224,11 @@ export default {
}, },
_bindToSelectedPost(action, binding) { _bindToSelectedPost(action, binding) {
const self = this; this.keyTrapper.bind(binding, () => this.sendToSelectedPost(action));
this.keyTrapper.bind(binding, function() {
self.sendToSelectedPost(action);
});
}, },
_bindToPath(path, binding) { _bindToPath(path, key) {
this.keyTrapper.bind(binding, function() { this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path));
DiscourseURL.routeTo(path);
});
}, },
_bindToClick(selector, binding) { _bindToClick(selector, binding) {

View File

@ -1,11 +1,8 @@
function applicable() { function applicable() {
// CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching) // This will apply hack on all iDevices
// Dolphin has a wierd user agent, rest seem a bit nitch
return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) && return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
navigator.userAgent.match(/Safari/g) && navigator.userAgent.match(/Safari/g);
!navigator.userAgent.match(/CriOS/g) &&
!navigator.userAgent.match(/OPiOS/g);
} }
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810 // per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
@ -17,6 +14,7 @@ function positioningWorkaround($fixedElement) {
const fixedElement = $fixedElement[0]; const fixedElement = $fixedElement[0];
var done = false; var done = false;
var originalScrollTop = 0;
var blurredNow = function(evt) { var blurredNow = function(evt) {
if (!done && _.include($(document.activeElement).parents(), fixedElement)) { if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
@ -25,8 +23,16 @@ function positioningWorkaround($fixedElement) {
} }
done = true; done = true;
fixedElement.parentElement.style.height = '';
$('#main-outlet').show();
$('header').show();
fixedElement.style.position = ''; fixedElement.style.position = '';
fixedElement.style.top = ''; fixedElement.style.top = '';
fixedElement.style.height = '';
$(window).scrollTop(originalScrollTop);
if (evt) { if (evt) {
evt.target.removeEventListener('blur', blurred); evt.target.removeEventListener('blur', blurred);
} }
@ -50,31 +56,23 @@ function positioningWorkaround($fixedElement) {
return; return;
} }
originalScrollTop = $(window).scrollTop();
// take care of body
$('#main-outlet').hide();
$('header').hide();
fixedElement.style.position = 'absolute'; fixedElement.style.position = 'absolute';
// get out of the way while opening keyboard // get out of the way while opening keyboard
fixedElement.style.top = '0px'; fixedElement.style.top = '0px';
fixedElement.style.height = parseInt(window.innerHeight*0.6) + "px";
fixedElement.parentElement.style.height = window.innerHeight + "px";
$(window).scrollTop(0);
// great ... iOS positions this yet again
// so lets take over if this happens
setTimeout(()=>$(window).scrollTop(0),500);
var iPadOffset = 0;
if (window.innerHeight > window.innerWidth && navigator.userAgent.match(/iPad/)) {
// there is no way to get virtual keyboard height
iPadOffset = 640 - $(fixedElement).height();
}
var oldScrollY = 0;
var positionElement = function(){
if (done) {
return;
}
if (Math.abs(oldScrollY - window.scrollY) < 20) {
return;
}
oldScrollY = window.scrollY;
fixedElement.style.top = window.scrollY + iPadOffset + 'px';
};
// position once, correctly, after keyboard is shown
setTimeout(positionElement, 500);
evt.preventDefault(); evt.preventDefault();
self.focus(); self.focus();

View File

@ -10,6 +10,9 @@ const ScreenTrack = Ember.Object.extend({
init() { init() {
this.reset(); this.reset();
// TODO: Move `ScreenTrack` to injection and remove this
this.set('topicTrackingState', Discourse.__container__.lookup('topic-tracking-state:main'));
}, },
start(topicId, topicController) { start(topicId, topicController) {
@ -110,7 +113,7 @@ const ScreenTrack = Ember.Object.extend({
highestSeenByTopic[topicId] = highestSeen; highestSeenByTopic[topicId] = highestSeen;
} }
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen); this.topicTrackingState.updateSeen(topicId, highestSeen);
if (!$.isEmptyObject(newTimings)) { if (!$.isEmptyObject(newTimings)) {
if (Discourse.User.current()) { if (Discourse.User.current()) {

View File

@ -86,4 +86,32 @@ function searchForTerm(term, opts) {
return promise; return promise;
} }
export default searchForTerm; const searchContextDescription = function(type, name){
if (type) {
switch(type) {
case 'topic':
return I18n.t('search.context.topic');
case 'user':
return I18n.t('search.context.user', {username: name});
case 'category':
return I18n.t('search.context.category', {category: name});
case 'private_messages':
return I18n.t('search.context.private_messages');
}
}
};
const getSearchKey = function(args){
return args.q + "|" + ((args.searchContext && args.searchContext.type) || "") + "|" +
((args.searchContext && args.searchContext.id) || "")
};
const isValidSearchTerm = function(searchTerm) {
if (searchTerm) {
return searchTerm.trim().length >= Discourse.SiteSettings.min_search_term_length;
} else {
return false;
}
};
export { searchForTerm, searchContextDescription, getSearchKey, isValidSearchTerm };

View File

@ -105,7 +105,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
It contains the logic necessary to route within a topic using replaceState to It contains the logic necessary to route within a topic using replaceState to
keep the history intact. keep the history intact.
**/ **/
routeTo: function(path, opts) { routeTo(path, opts) {
if (Em.isEmpty(path)) { return; } if (Em.isEmpty(path)) { return; }
if (Discourse.get('requiresRefresh')) { if (Discourse.get('requiresRefresh')) {
@ -122,6 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
// Scroll to the same page, different anchor // Scroll to the same page, different anchor
if (path.indexOf('#') === 0) { if (path.indexOf('#') === 0) {
this.scrollToId(path); this.scrollToId(path);
history.replaceState(undefined, undefined, path);
return; return;
} }
@ -271,7 +272,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
// This has been extracted so it can be tested. // This has been extracted so it can be tested.
origin: function() { origin: function() {
return window.location.origin; return window.location.origin + (Discourse.BaseUri === "/" ? '' : Discourse.BaseUri);
}, },
/** /**

View File

@ -89,7 +89,7 @@ export default function userSearch(options) {
return new Ember.RSVP.Promise(function(resolve) { return new Ember.RSVP.Promise(function(resolve) {
// TODO site setting for allowed regex in username // TODO site setting for allowed regex in username
if (term.match(/[^a-zA-Z0-9_\.]/)) { if (term.match(/[^a-zA-Z0-9_\.\-]/)) {
resolve([]); resolve([]);
return; return;
} }

View File

@ -97,7 +97,10 @@ Discourse.Utilities = {
// Strip out any .click elements from the HTML before converting it to text // Strip out any .click elements from the HTML before converting it to text
var div = document.createElement('div'); var div = document.createElement('div');
div.innerHTML = html; div.innerHTML = html;
$('.clicks', $(div)).remove(); var $div = $(div);
// Find all emojis and replace with its title attribute.
$div.find('img.emoji').replaceWith(function() { return this.title });
$('.clicks', $div).remove();
var text = div.textContent || div.innerText || ""; var text = div.textContent || div.innerText || "";
return String(text).trim(); return String(text).trim();
@ -212,6 +215,10 @@ Discourse.Utilities = {
} }
}, },
getUploadPlaceholder: function(filename) {
return "[" + I18n.t("uploading_filename", { filename: filename }) + "]() ";
},
isAnImage: function(path) { isAnImage: function(path) {
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path); return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
}, },

View File

@ -36,7 +36,7 @@ export default Ember.Mixin.create({
} }
promise.then(function(result) { promise.then(function(result) {
if (result && result.topic_ids) { if (result && result.topic_ids) {
const tracker = Discourse.TopicTrackingState.current(); const tracker = self.topicTrackingState;
result.topic_ids.forEach(function(t) { result.topic_ids.forEach(function(t) {
tracker.removeTopic(t); tracker.removeTopic(t);
}); });

View File

@ -1,5 +1,6 @@
import Eyeline from 'discourse/lib/eyeline'; import Eyeline from 'discourse/lib/eyeline';
import Scrolling from 'discourse/mixins/scrolling'; import Scrolling from 'discourse/mixins/scrolling';
import { on } from 'ember-addons/ember-computed-decorators';
// Provides the ability to load more items for a view which is scrolled to the bottom. // Provides the ability to load more items for a view which is scrolled to the bottom.
export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
@ -9,15 +10,23 @@ export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
if (eyeline) { eyeline.update(); } if (eyeline) { eyeline.update(); }
}, },
_bindEyeline: function() { loadMoreUnlessFull() {
if (this.screenNotFull()) {
this.send("loadMore");
}
},
@on("didInsertElement")
_bindEyeline() {
const eyeline = new Eyeline(this.get('eyelineSelector') + ":last"); const eyeline = new Eyeline(this.get('eyelineSelector') + ":last");
this.set('eyeline', eyeline); this.set('eyeline', eyeline);
eyeline.on('sawBottom', () => this.send('loadMore')); eyeline.on('sawBottom', () => this.send('loadMore'));
this.bindScrolling(); this.bindScrolling();
}.on('didInsertElement'), },
_removeEyeline: function() { @on("willDestroyElement")
_removeEyeline() {
this.unbindScrolling(); this.unbindScrolling();
}.on('willDestroyElement') }
}); });

View File

@ -3,7 +3,7 @@ export default Em.Mixin.create({
needs: ['modal'], needs: ['modal'],
flash: function(message, messageClass) { flash(message, messageClass) {
this.set('flashMessage', Em.Object.create({ message, messageClass })); this.set('flashMessage', Em.Object.create({ message, messageClass }));
} }
}); });

View File

@ -6,16 +6,20 @@ import debounce from 'discourse/lib/debounce';
easier. easier.
**/ **/
const ScrollingDOMMethods = { const ScrollingDOMMethods = {
bindOnScroll: function(onScrollMethod, name) { bindOnScroll(onScrollMethod, name) {
name = name || 'default'; name = name || 'default';
$(document).bind('touchmove.discourse-' + name, onScrollMethod); $(document).bind(`touchmove.discourse-${name}`, onScrollMethod);
$(window).bind('scroll.discourse-' + name, onScrollMethod); $(window).bind(`scroll.discourse-${name}`, onScrollMethod);
}, },
unbindOnScroll: function(name) { unbindOnScroll(name) {
name = name || 'default'; name = name || 'default';
$(window).unbind('scroll.discourse-' + name); $(window).unbind(`scroll.discourse-${name}`);
$(document).unbind('touchmove.discourse-' + name); $(document).unbind(`touchmove.discourse-${name}`);
},
screenNotFull() {
return $(window).height() >= $(document).height();
} }
}; };
@ -23,16 +27,15 @@ const Scrolling = Ember.Mixin.create({
// Begin watching for scroll events. By default they will be called at max every 100ms. // Begin watching for scroll events. By default they will be called at max every 100ms.
// call with {debounce: N} for a diff time // call with {debounce: N} for a diff time
bindScrolling: function(opts) { bindScrolling(opts) {
opts = opts || {debounce: 100}; opts = opts || { debounce: 100 };
// So we can not call the scrolled event while transitioning // So we can not call the scrolled event while transitioning
const router = Discourse.__container__.lookup('router:main').router; const router = Discourse.__container__.lookup('router:main').router;
const self = this; let onScrollMethod = () => {
var onScrollMethod = function() {
if (router.activeTransition) { return; } if (router.activeTransition) { return; }
return Em.run.scheduleOnce('afterRender', self, 'scrolled'); return Ember.run.scheduleOnce('afterRender', this, 'scrolled');
}; };
if (opts.debounce) { if (opts.debounce) {
@ -40,10 +43,11 @@ const Scrolling = Ember.Mixin.create({
} }
ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
Em.run.scheduleOnce('afterRender', onScrollMethod);
}, },
unbindScrolling: function(name) { screenNotFull: () => ScrollingDOMMethods.screenNotFull(),
unbindScrolling(name) {
ScrollingDOMMethods.unbindOnScroll(name); ScrollingDOMMethods.unbindOnScroll(name);
} }
}); });

View File

@ -8,10 +8,11 @@ export default {
return `${type}_${hashedArgs}`; return `${type}_${hashedArgs}`;
}, },
findStale(store, type, findArgs) { findStale(store, type, findArgs, opts) {
const staleResult = new StaleResult(); const staleResult = new StaleResult();
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
try { try {
const stored = this.keyValueStore.getItem(this.storageKey(type, findArgs)); const stored = this.keyValueStore.getItem(key);
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
staleResult.setResults(parsed); staleResult.setResults(parsed);
@ -22,9 +23,11 @@ export default {
return staleResult; return staleResult;
}, },
find(store, type, findArgs) { find(store, type, findArgs, opts) {
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
return this._super(store, type, findArgs).then((results) => { return this._super(store, type, findArgs).then((results) => {
this.keyValueStore.setItem(this.storageKey(type, findArgs), JSON.stringify(results)); this.keyValueStore.setItem(key, JSON.stringify(results));
return results; return results;
}); });
} }

View File

@ -0,0 +1,60 @@
const CategoryList = Ember.ArrayProxy.extend({
init() {
this.set('content', []);
this._super();
}
});
CategoryList.reopenClass({
categoriesFrom(store, result) {
const categories = Discourse.CategoryList.create();
const users = Discourse.Model.extractByKey(result.featured_users, Discourse.User);
const list = Discourse.Category.list();
result.category_list.categories.forEach(function(c) {
if (c.parent_category_id) {
c.parentCategory = list.findBy('id', c.parent_category_id);
}
if (c.subcategory_ids) {
c.subcategories = c.subcategory_ids.map(scid => list.findBy('id', parseInt(scid, 10)));
}
if (c.featured_user_ids) {
c.featured_users = c.featured_user_ids.map(u => users[u]);
}
if (c.topics) {
c.topics = c.topics.map(t => Discourse.Topic.create(t));
}
categories.pushObject(store.createRecord('category', c));
});
return categories;
},
listForParent(store, category) {
return Discourse.ajax(`/categories.json?parent_category_id=${category.get("id")}`).then(result => {
return Discourse.CategoryList.create({
categories: this.categoriesFrom(store, result),
parentCategory: category
});
});
},
list(store) {
const getCategories = () => Discourse.ajax("/categories.json");
return PreloadStore.getAndRemove("categories_list", getCategories).then(result => {
return Discourse.CategoryList.create({
categories: this.categoriesFrom(store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
draft_key: result.category_list.draft_key,
draft: result.category_list.draft,
draft_sequence: result.category_list.draft_sequence
});
});
}
});
export default CategoryList;

View File

@ -1,17 +1,24 @@
Discourse.Category = Discourse.Model.extend({ import RestModel from 'discourse/models/rest';
import { on } from 'ember-addons/ember-computed-decorators';
init: function() { const Category = RestModel.extend({
this._super();
var availableGroups = Em.A(this.get("available_groups"));
@on('init')
setupGroupsAndPermissions() {
const availableGroups = this.get('available_groups');
if (!availableGroups) { return; }
this.set("availableGroups", availableGroups); this.set("availableGroups", availableGroups);
this.set("permissions", Em.A(_.map(this.group_permissions, function(elem){
const groupPermissions = this.get('group_permissions');
if (groupPermissions) {
this.set('permissions', groupPermissions.map((elem) => {
availableGroups.removeObject(elem.group_name); availableGroups.removeObject(elem.group_name);
return { return {
group_name: elem.group_name, group_name: elem.group_name,
permission: Discourse.PermissionType.create({id: elem.permission_type}) permission: Discourse.PermissionType.create({id: elem.permission_type})
}; };
}))); }));
}
}, },
availablePermissions: function(){ availablePermissions: function(){
@ -26,7 +33,7 @@ Discourse.Category = Discourse.Model.extend({
}.property('id'), }.property('id'),
url: function() { url: function() {
return Discourse.getURL("/c/") + Discourse.Category.slugFor(this); return Discourse.getURL("/c/") + Category.slugFor(this);
}.property('name'), }.property('name'),
fullSlug: function() { fullSlug: function() {
@ -77,7 +84,8 @@ Discourse.Category = Discourse.Model.extend({
background_url: this.get('background_url'), background_url: this.get('background_url'),
allow_badges: this.get('allow_badges'), allow_badges: this.get('allow_badges'),
custom_fields: this.get('custom_fields'), custom_fields: this.get('custom_fields'),
topic_template: this.get('topic_template') topic_template: this.get('topic_template'),
suppress_from_homepage: this.get('suppress_from_homepage'),
}, },
type: this.get('id') ? 'PUT' : 'POST' type: this.get('id') ? 'PUT' : 'POST'
}); });
@ -128,16 +136,12 @@ Discourse.Category = Discourse.Model.extend({
} }
}.property('topics'), }.property('topics'),
topicTrackingState: function(){ unreadTopics: function() {
return Discourse.TopicTrackingState.current(); return this.topicTrackingState.countUnread(this.get('id'));
}.property(),
unreadTopics: function(){
return this.get('topicTrackingState').countUnread(this.get('id'));
}.property('topicTrackingState.messageCount'), }.property('topicTrackingState.messageCount'),
newTopics: function(){ newTopics: function() {
return this.get('topicTrackingState').countNew(this.get('id')); return this.topicTrackingState.countNew(this.get('id'));
}.property('topicTrackingState.messageCount'), }.property('topicTrackingState.messageCount'),
topicStatsTitle: function() { topicStatsTitle: function() {
@ -192,83 +196,78 @@ Discourse.Category = Discourse.Model.extend({
var _uncategorized; var _uncategorized;
Discourse.Category.reopenClass({ Category.reopenClass({
findUncategorized: function() { findUncategorized() {
_uncategorized = _uncategorized || Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); _uncategorized = _uncategorized || Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
return _uncategorized; return _uncategorized;
}, },
slugFor: function(category) { slugFor(category) {
if (!category) return ""; if (!category) return "";
var parentCategory = Em.get(category, 'parentCategory'), const parentCategory = Em.get(category, 'parentCategory');
result = ""; let result = "";
if (parentCategory) { if (parentCategory) {
result = Discourse.Category.slugFor(parentCategory) + "/"; result = Category.slugFor(parentCategory) + "/";
} }
var id = Em.get(category, 'id'), const id = Em.get(category, 'id'),
slug = Em.get(category, 'slug'); slug = Em.get(category, 'slug');
if (!slug || slug.trim().length === 0) return result + id + "-category"; return !slug || slug.trim().length === 0 ? `${result}${id}-category` : result + slug;
return result + slug;
}, },
list: function() { list() {
if (Discourse.SiteSettings.fixed_category_positions) { return Discourse.SiteSettings.fixed_category_positions ?
return Discourse.Site.currentProp('categories'); Discourse.Site.currentProp('categories') :
} else { Discourse.Site.currentProp('sortedCategories');
return Discourse.Site.currentProp('sortedCategories');
}
}, },
listByActivity: function() { listByActivity() {
return Discourse.Site.currentProp('sortedCategories'); return Discourse.Site.currentProp('sortedCategories');
}, },
idMap: function() { idMap() {
return Discourse.Site.currentProp('categoriesById'); return Discourse.Site.currentProp('categoriesById');
}, },
findSingleBySlug: function(slug) { findSingleBySlug(slug) {
return Discourse.Category.list().find(function(c) { return Category.list().find(c => Category.slugFor(c) === slug);
return Discourse.Category.slugFor(c) === slug;
});
}, },
findById: function(id) { findById(id) {
if (!id) { return; } if (!id) { return; }
return Discourse.Category.idMap()[id]; return Category.idMap()[id];
}, },
findByIds: function(ids){ findByIds(ids) {
var categories = []; const categories = [];
_.each(ids, function(id){ _.each(ids, id => {
var found = Discourse.Category.findById(id); const found = Category.findById(id);
if(found){ if (found) {
categories.push(found); categories.push(found);
} }
}); });
return categories; return categories;
}, },
findBySlug: function(slug, parentSlug) { findBySlug(slug, parentSlug) {
var categories = Discourse.Category.list(), const categories = Category.list();
category; let category;
if (parentSlug) { if (parentSlug) {
var parentCategory = Discourse.Category.findSingleBySlug(parentSlug); const parentCategory = Category.findSingleBySlug(parentSlug);
if (parentCategory) { if (parentCategory) {
if (slug === 'none') { return parentCategory; } if (slug === 'none') { return parentCategory; }
category = categories.find(function(item) { category = categories.find(item => {
return item && item.get('parentCategory') === parentCategory && Discourse.Category.slugFor(item) === (parentSlug + "/" + slug); return item && item.get('parentCategory') === parentCategory && Category.slugFor(item) === (parentSlug + "/" + slug);
}); });
} }
} else { } else {
category = Discourse.Category.findSingleBySlug(slug); category = Category.findSingleBySlug(slug);
// If we have a parent category, we need to enforce it // If we have a parent category, we need to enforce it
if (category && category.get('parentCategory')) return; if (category && category.get('parentCategory')) return;
@ -282,9 +281,9 @@ Discourse.Category.reopenClass({
return category; return category;
}, },
reloadById: function(id) { reloadById(id) {
return Discourse.ajax("/c/" + id + "/show.json").then(function (result) { return Discourse.ajax(`/c/${id}/show.json`);
return Discourse.Category.create(result.category);
});
} }
}); });
export default Category;

View File

@ -1,68 +0,0 @@
Discourse.CategoryList = Ember.ArrayProxy.extend({
init: function() {
this.set('content', []);
this._super();
}
});
Discourse.CategoryList.reopenClass({
categoriesFrom: function(result) {
var categories = Discourse.CategoryList.create(),
users = Discourse.Model.extractByKey(result.featured_users, Discourse.User),
list = Discourse.Category.list();
result.category_list.categories.forEach(function(c) {
if (c.parent_category_id) {
c.parentCategory = list.findBy('id', c.parent_category_id);
}
if (c.subcategory_ids) {
c.subcategories = c.subcategory_ids.map(function(scid) { return list.findBy('id', parseInt(scid, 10)); });
}
if (c.featured_user_ids) {
c.featured_users = c.featured_user_ids.map(function(u) {
return users[u];
});
}
if (c.topics) {
c.topics = c.topics.map(function(t) {
return Discourse.Topic.create(t);
});
}
categories.pushObject(Discourse.Category.create(c));
});
return categories;
},
listForParent: function(category) {
var self = this;
return Discourse.ajax('/categories.json?parent_category_id=' + category.get('id')).then(function(result) {
return Discourse.CategoryList.create({
categories: self.categoriesFrom(result),
parentCategory: category
});
});
},
list: function() {
var self = this;
return PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json");
}).then(function(result) {
return Discourse.CategoryList.create({
categories: self.categoriesFrom(result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
draft_key: result.category_list.draft_key,
draft: result.category_list.draft,
draft_sequence: result.category_list.draft_sequence
});
});
}
});

View File

@ -3,6 +3,7 @@ import Topic from 'discourse/models/topic';
import { throwAjaxError } from 'discourse/lib/ajax-error'; import { throwAjaxError } from 'discourse/lib/ajax-error';
import Quote from 'discourse/lib/quote'; import Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft'; import Draft from 'discourse/models/draft';
import computed from 'ember-addons/ember-computed-decorators';
const CLOSED = 'closed', const CLOSED = 'closed',
SAVING = 'saving', SAVING = 'saving',
@ -23,6 +24,7 @@ const CLOSED = 'closed',
category: 'categoryId', category: 'categoryId',
topic_id: 'topic.id', topic_id: 'topic.id',
is_warning: 'isWarning', is_warning: 'isWarning',
whisper: 'whisper',
archetype: 'archetypeId', archetype: 'archetypeId',
target_usernames: 'targetUsernames', target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime', typing_duration_msecs: 'typingTime',
@ -35,15 +37,42 @@ const CLOSED = 'closed',
}; };
const Composer = RestModel.extend({ const Composer = RestModel.extend({
_categoryId: null,
archetypes: function() { archetypes: function() {
return this.site.get('archetypes'); return this.site.get('archetypes');
}.property(), }.property(),
@computed
categoryId: {
get() { return this._categoryId; },
// We wrap categoryId this way so we can fire `applyTopicTemplate` with
// the previous value as well as the new value
set(categoryId) {
const oldCategoryId = this._categoryId;
if (Ember.isEmpty(categoryId)) { categoryId = null; }
this._categoryId = categoryId;
if (oldCategoryId !== categoryId) {
this.applyTopicTemplate(oldCategoryId, categoryId);
}
return categoryId;
}
},
creatingTopic: Em.computed.equal('action', CREATE_TOPIC), creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
showCategoryChooser: function(){
const manyCategories = Discourse.Category.list().length > 1;
const hasOptions = this.get('archetype.hasOptions');
return !this.get('privateMessage') && (hasOptions || manyCategories);
}.property('privateMessage'),
privateMessage: function(){ privateMessage: function(){
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message'; return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
}.property('creatingPrivateMessage', 'topic'), }.property('creatingPrivateMessage', 'topic'),
@ -56,6 +85,7 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal('composeState', OPEN), viewOpen: Em.computed.equal('composeState', OPEN),
viewDraft: Em.computed.equal('composeState', DRAFT), viewDraft: Em.computed.equal('composeState', DRAFT),
composeStateChanged: function() { composeStateChanged: function() {
var oldOpen = this.get('composerOpened'); var oldOpen = this.get('composerOpened');
@ -339,20 +369,24 @@ const Composer = RestModel.extend({
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
}, },
applyTopicTemplate: function() { applyTopicTemplate(oldCategoryId, categoryId) {
if (this.get('action') !== CREATE_TOPIC) { return; } if (this.get('action') !== CREATE_TOPIC) { return; }
if (!Ember.isEmpty(this.get('reply'))) { return; } let reply = this.get('reply');
const categoryId = this.get('categoryId'); // If the user didn't change the template, clear it
const category = this.site.categories.find((c) => c.get('id') === categoryId); if (oldCategoryId) {
const oldCat = this.site.categories.findProperty('id', oldCategoryId);
if (oldCat && (oldCat.get('topic_template') === reply)) {
reply = "";
}
}
if (!Ember.isEmpty(reply)) { return; }
const category = this.site.categories.findProperty('id', categoryId);
if (category) { if (category) {
const topicTemplate = category.get('topic_template'); this.set('reply', category.get('topic_template') || "");
if (!Ember.isEmpty(topicTemplate)) {
this.set('reply', topicTemplate);
} }
} },
}.observes('categoryId'),
/* /*
Open a composer Open a composer
@ -397,14 +431,22 @@ const Composer = RestModel.extend({
} }
} }
const categoryId = opts.categoryId || this.get('topic.category.id');
this.setProperties({ this.setProperties({
categoryId,
archetypeId: opts.archetypeId || this.site.get('default_archetype'), archetypeId: opts.archetypeId || this.site.get('default_archetype'),
metaData: opts.metaData ? Em.Object.create(opts.metaData) : null, metaData: opts.metaData ? Em.Object.create(opts.metaData) : null,
reply: opts.reply || this.get("reply") || "" reply: opts.reply || this.get("reply") || ""
}); });
// We set the category id separately for topic templates on opening of composer
this.set('categoryId', opts.categoryId || this.get('topic.category.id'));
if (!this.get('categoryId') && this.get('creatingTopic')) {
const categories = Discourse.Category.list();
if (categories.length === 1) {
this.set('categoryId', categories[0].get('id'));
}
}
if (opts.postId) { if (opts.postId) {
this.set('loading', true); this.set('loading', true);
this.store.find('post', opts.postId).then(function(post) { this.store.find('post', opts.postId).then(function(post) {
@ -529,6 +571,9 @@ const Composer = RestModel.extend({
let addedToStream = false; let addedToStream = false;
const postTypes = this.site.get('post_types');
const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular;
// Build the post object // Build the post object
const createdPost = this.store.createRecord('post', { const createdPost = this.store.createRecord('post', {
imageSizes: opts.imageSizes, imageSizes: opts.imageSizes,
@ -539,9 +584,9 @@ const Composer = RestModel.extend({
username: user.get('username'), username: user.get('username'),
user_id: user.get('id'), user_id: user.get('id'),
user_title: user.get('title'), user_title: user.get('title'),
uploaded_avatar_id: user.get('uploaded_avatar_id'), avatar_template: user.get('avatar_template'),
user_custom_fields: user.get('custom_fields'), user_custom_fields: user.get('custom_fields'),
post_type: this.site.get('post_types.regular'), post_type: postType,
actions_summary: [], actions_summary: [],
moderator: user.get('moderator'), moderator: user.get('moderator'),
admin: user.get('admin'), admin: user.get('admin'),
@ -559,7 +604,7 @@ const Composer = RestModel.extend({
reply_to_post_number: post.get('post_number'), reply_to_post_number: post.get('post_number'),
reply_to_user: { reply_to_user: {
username: post.get('username'), username: post.get('username'),
uploaded_avatar_id: post.get('uploaded_avatar_id') avatar_template: post.get('avatar_template')
} }
}); });
} }
@ -690,47 +735,6 @@ const Composer = RestModel.extend({
Composer.reopenClass({ Composer.reopenClass({
open(opts) {
const composer = Composer.create();
composer.open(opts);
return composer;
},
loadDraft(opts) {
opts = opts || {};
let draft = opts.draft;
const draftKey = opts.draftKey;
const draftSequence = opts.draftSequence;
try {
if (draft && typeof draft === 'string') {
draft = JSON.parse(draft);
}
} catch (error) {
draft = null;
Draft.clear(draftKey, draftSequence);
}
if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) {
return this.open({
draftKey,
draftSequence,
action: draft.action,
title: draft.title,
categoryId: draft.categoryId || opts.categoryId,
postId: draft.postId,
archetypeId: draft.archetypeId,
reply: draft.reply,
metaData: draft.metaData,
usernames: draft.usernames,
draft: true,
composerState: DRAFT,
composerTime: draft.composerTime,
typingTime: draft.typingTime
});
}
},
// TODO: Replace with injection // TODO: Replace with injection
create(args) { create(args) {
args = args || {}; args = args || {};

View File

@ -20,10 +20,6 @@ const NavItem = Discourse.Model.extend({
return I18n.t("filters." + name.replace("/", ".") + ".title", extra); return I18n.t("filters." + name.replace("/", ".") + ".title", extra);
}.property('categoryName', 'name', 'count'), }.property('categoryName', 'name', 'count'),
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),
categoryName: function() { categoryName: function() {
var split = this.get('name').split('/'); var split = this.get('name').split('/');
return split[0] === 'category' ? split[1] : null; return split[0] === 'category' ? split[1] : null;
@ -100,26 +96,24 @@ NavItem.reopenClass({
extra = cb.call(self, text, opts); extra = cb.call(self, text, opts);
_.merge(args, extra); _.merge(args, extra);
}); });
return Discourse.NavItem.create(args);
const store = Discourse.__container__.lookup('store:main');
return store.createRecord('nav-item', args);
}, },
buildList(category, args) { buildList(category, args) {
args = args || {}; args = args || {};
if (category) { args.category = category } if (category) { args.category = category }
var items = Discourse.SiteSettings.top_menu.split("|"); let items = Discourse.SiteSettings.top_menu.split("|");
if (args.filterMode && !_.some(items, function(i){ if (args.filterMode && !_.some(items, i => i.indexOf(args.filterMode) !== -1)) {
return i.indexOf(args.filterMode) !== -1;
})) {
items.push(args.filterMode); items.push(args.filterMode);
} }
return items.map(function(i) { return items.map(i => Discourse.NavItem.fromText(i, args))
return Discourse.NavItem.fromText(i, args); .filter(i => i !== null && !(category && i.get("name").indexOf("categor") === 0));
}).filter(function(i) {
return i !== null && !(category && i.get("name").indexOf("categor") === 0);
});
} }
}); });

View File

@ -5,10 +5,7 @@ function calcDayDiff(p1, p2) {
if (!p1) { return; } if (!p1) { return; }
const date = p1.get('created_at'); const date = p1.get('created_at');
if (date) { if (date && p2) {
if (p2) {
const numDiff = p1.get('post_number') - p2.get('post_number');
if (numDiff === 1) {
const lastDate = p2.get('created_at'); const lastDate = p2.get('created_at');
if (lastDate) { if (lastDate) {
const delta = new Date(date).getTime() - new Date(lastDate).getTime(); const delta = new Date(date).getTime() - new Date(lastDate).getTime();
@ -17,8 +14,6 @@ function calcDayDiff(p1, p2) {
p1.set('daysSincePrevious', days); p1.set('daysSincePrevious', days);
} }
} }
}
}
} }
const PostStream = RestModel.extend({ const PostStream = RestModel.extend({
@ -282,13 +277,12 @@ const PostStream = RestModel.extend({
fillGapAfter(post, gap) { fillGapAfter(post, gap) {
const postId = post.get('id'), const postId = post.get('id'),
stream = this.get('stream'), stream = this.get('stream'),
idx = stream.indexOf(postId), idx = stream.indexOf(postId);
self = this;
if (idx !== -1) { if (idx !== -1) {
stream.pushObjects(gap); stream.pushObjects(gap);
return this.appendMore().then(function() { return this.appendMore().then(() => {
self.get('stream').enumerableContentDidChange(); this.get('stream').enumerableContentDidChange();
}); });
} }
return Ember.RSVP.resolve(); return Ember.RSVP.resolve();
@ -296,24 +290,18 @@ const PostStream = RestModel.extend({
// Appends the next window of posts to the stream. Call it when scrolling downwards. // Appends the next window of posts to the stream. Call it when scrolling downwards.
appendMore() { appendMore() {
const self = this;
// Make sure we can append more posts // Make sure we can append more posts
if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); } if (!this.get('canAppendMore')) { return Ember.RSVP.resolve(); }
const postIds = self.get('nextWindow'); const postIds = this.get('nextWindow');
if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); } if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); }
self.set('loadingBelow', true); this.set('loadingBelow', true);
const stopLoading = function() { const stopLoading = () => this.set('loadingBelow', false);
self.set('loadingBelow', false);
};
return self.findPostsByIds(postIds).then(function(posts) { return this.findPostsByIds(postIds).then((posts) => {
posts.forEach(function(p) { posts.forEach(p => this.appendPost(p));
self.appendPost(p);
});
stopLoading(); stopLoading();
}, stopLoading); }, stopLoading);
}, },
@ -685,6 +673,12 @@ const PostStream = RestModel.extend({
const postIdentityMap = this.get('postIdentityMap'), const postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(post.get('id')); existing = postIdentityMap.get(post.get('id'));
// Update the `highest_post_number` if this post is higher.
const postNumber = post.get('post_number');
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
this.set('topic.highest_post_number', postNumber);
}
if (existing) { if (existing) {
// If the post is in the identity map, update it and return the old reference. // If the post is in the identity map, update it and return the old reference.
existing.updateFromPost(post); existing.updateFromPost(post);
@ -693,12 +687,6 @@ const PostStream = RestModel.extend({
post.set('topic', this.get('topic')); post.set('topic', this.get('topic'));
postIdentityMap.set(post.get('id'), post); postIdentityMap.set(post.get('id'), post);
// Update the `highest_post_number` if this post is higher.
const postNumber = post.get('post_number');
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
this.set('topic.highest_post_number', postNumber);
}
} }
return post; return post;
}, },

View File

@ -1,7 +1,7 @@
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
import ActionSummary from 'discourse/models/action-summary'; import ActionSummary from 'discourse/models/action-summary';
import { url, fmt, propertyEqual } from 'discourse/lib/computed'; import { url, propertyEqual } from 'discourse/lib/computed';
import Quote from 'discourse/lib/quote'; import Quote from 'discourse/lib/quote';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
@ -77,29 +77,18 @@ const Post = RestModel.extend({
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'), topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),
hasHistory: Em.computed.gt('version', 1), hasHistory: Em.computed.gt('version', 1),
postElementId: fmt('post_number', 'post_%@'),
canViewRawEmail: function() { canViewRawEmail: function() {
return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff'); return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff');
}.property("user_id"), }.property("user_id"),
wikiChanged: function() { updatePostField(field, value) {
const data = { wiki: this.get("wiki") }; const data = {};
this._updatePost("wiki", data); data[field] = value;
}.observes('wiki'),
postTypeChanged: function () { Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => {
const data = { post_type: this.get("post_type") }; this.set(field, value);
this._updatePost("post_type", data); this.incrementProperty("version");
}.observes("post_type"),
_updatePost(field, data) {
const self = this;
Discourse.ajax("/posts/" + this.get("id") + "/" + field, {
type: "PUT",
data: data
}).then(function () {
self.incrementProperty("version");
}).catch(popupAjaxError); }).catch(popupAjaxError);
}, },

View File

@ -78,13 +78,10 @@ RestModel.reopenClass({
create(args) { create(args) {
args = args || {}; args = args || {};
if (!args.store || !args.keyValueStore) { if (!args.store) {
const container = Discourse.__container__; const container = Discourse.__container__;
// Ember.warn('Use `store.createRecord` to create records instead of `.create()`'); // Ember.warn('Use `store.createRecord` to create records instead of `.create()`');
args.store = container.lookup('store:main'); args.store = container.lookup('store:main');
// TODO: Remove this when composer is using the store fully
args.keyValueStore = container.lookup('key-value-store:main');
} }
args.__munge = this.munge; args.__munge = this.munge;

View File

@ -1,35 +1,37 @@
import computed from "ember-addons/ember-computed-decorators";
import Archetype from 'discourse/models/archetype'; import Archetype from 'discourse/models/archetype';
import PostActionType from 'discourse/models/post-action-type'; import PostActionType from 'discourse/models/post-action-type';
import Singleton from 'discourse/mixins/singleton'; import Singleton from 'discourse/mixins/singleton';
import RestModel from 'discourse/models/rest';
const Site = Discourse.Model.extend({ const Site = RestModel.extend({
isReadOnly: Em.computed.alias('is_readonly'), isReadOnly: Em.computed.alias('is_readonly'),
notificationLookup: function() { @computed("notification_types")
notificationLookup(notificationTypes) {
const result = []; const result = [];
_.each(this.get('notification_types'), function(v,k) { _.each(notificationTypes, (v, k) => result[v] = k);
result[v] = k;
});
return result; return result;
}.property('notification_types'), },
flagTypes: function() { @computed("post_action_types.@each")
flagTypes() {
const postActionTypes = this.get('post_action_types'); const postActionTypes = this.get('post_action_types');
if (!postActionTypes) return []; if (!postActionTypes) return [];
return postActionTypes.filterProperty('is_flag', true); return postActionTypes.filterProperty('is_flag', true);
}.property('post_action_types.@each'), },
topicCountDesc: ['topic_count:desc'], topicCountDesc: ['topic_count:desc'],
categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'), categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'),
// Sort subcategories under parents // Sort subcategories under parents
sortedCategories: function() { @computed("categoriesByCount", "categories.@each")
const cats = this.get('categoriesByCount'), sortedCategories(cats) {
result = [], const result = [],
remaining = {}; remaining = {};
cats.forEach(function(c) { cats.forEach(c => {
const parentCategoryId = parseInt(c.get('parent_category_id'), 10); const parentCategoryId = parseInt(c.get('parent_category_id'), 10);
if (!parentCategoryId) { if (!parentCategoryId) {
result.pushObject(c); result.pushObject(c);
@ -39,17 +41,17 @@ const Site = Discourse.Model.extend({
} }
}); });
Ember.keys(remaining).forEach(function(parentCategoryId) { Ember.keys(remaining).forEach(parentCategoryId => {
const category = result.findBy('id', parseInt(parentCategoryId, 10)), const category = result.findBy('id', parseInt(parentCategoryId, 10)),
index = result.indexOf(category); index = result.indexOf(category);
if (index !== -1) { if (index !== -1) {
result.replace(index+1, 0, remaining[parentCategoryId]); result.replace(index + 1, 0, remaining[parentCategoryId]);
} }
}); });
return result; return result;
}.property("categories.@each"), },
postActionTypeById(id) { postActionTypeById(id) {
return this.get("postActionByIdLookup.action" + id); return this.get("postActionByIdLookup.action" + id);
@ -80,7 +82,7 @@ const Site = Discourse.Model.extend({
existingCategory.setProperties(newCategory); existingCategory.setProperties(newCategory);
} else { } else {
// TODO insert in right order? // TODO insert in right order?
newCategory = Discourse.Category.create(newCategory); newCategory = this.store.createRecord('category', newCategory);
categories.pushObject(newCategory); categories.pushObject(newCategory);
this.get('categoriesById')[categoryId] = newCategory; this.get('categoriesById')[categoryId] = newCategory;
} }
@ -91,20 +93,20 @@ Site.reopenClass(Singleton, {
// The current singleton will retrieve its attributes from the `PreloadStore`. // The current singleton will retrieve its attributes from the `PreloadStore`.
createCurrent() { createCurrent() {
return Site.create(PreloadStore.get('site')); const store = Discourse.__container__.lookup('store:main');
return store.createRecord('site', PreloadStore.get('site'));
}, },
create() { create() {
const result = this._super.apply(this, arguments); const result = this._super.apply(this, arguments);
const store = result.store;
if (result.categories) { if (result.categories) {
result.categoriesById = {}; result.categoriesById = {};
result.categories = _.map(result.categories, function(c) { result.categories = _.map(result.categories, c => result.categoriesById[c.id] = store.createRecord('category', c));
return result.categoriesById[c.id] = Discourse.Category.create(c);
});
// Associate the categories with their parents // Associate the categories with their parents
result.categories.forEach(function (c) { result.categories.forEach(c => {
if (c.get('parent_category_id')) { if (c.get('parent_category_id')) {
c.set('parentCategory', result.categoriesById[c.get('parent_category_id')]); c.set('parentCategory', result.categoriesById[c.get('parent_category_id')]);
} }
@ -112,16 +114,13 @@ Site.reopenClass(Singleton, {
} }
if (result.trust_levels) { if (result.trust_levels) {
result.trustLevels = result.trust_levels.map(function (tl) { result.trustLevels = result.trust_levels.map(tl => Discourse.TrustLevel.create(tl));
return Discourse.TrustLevel.create(tl);
});
delete result.trust_levels; delete result.trust_levels;
} }
if (result.post_action_types) { if (result.post_action_types) {
result.postActionByIdLookup = Em.Object.create(); result.postActionByIdLookup = Em.Object.create();
result.post_action_types = _.map(result.post_action_types,function(p) { result.post_action_types = _.map(result.post_action_types, p => {
const actionType = PostActionType.create(p); const actionType = PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType); result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType; return actionType;
@ -130,7 +129,7 @@ Site.reopenClass(Singleton, {
if (result.topic_flag_types) { if (result.topic_flag_types) {
result.topicFlagByIdLookup = Em.Object.create(); result.topicFlagByIdLookup = Em.Object.create();
result.topic_flag_types = _.map(result.topic_flag_types,function(p) { result.topic_flag_types = _.map(result.topic_flag_types, p => {
const actionType = PostActionType.create(p); const actionType = PostActionType.create(p);
result.topicFlagByIdLookup.set("action" + p.id, actionType); result.topicFlagByIdLookup.set("action" + p.id, actionType);
return actionType; return actionType;
@ -138,16 +137,14 @@ Site.reopenClass(Singleton, {
} }
if (result.archetypes) { if (result.archetypes) {
result.archetypes = _.map(result.archetypes,function(a) { result.archetypes = _.map(result.archetypes, a => {
a.site = result; a.site = result;
return Archetype.create(a); return Archetype.create(a);
}); });
} }
if (result.user_fields) { if (result.user_fields) {
result.user_fields = result.user_fields.map(function(uf) { result.user_fields = result.user_fields.map(uf => Ember.Object.create(uf));
return Ember.Object.create(uf);
});
} }
return result; return result;

View File

@ -71,18 +71,18 @@ export default Ember.Object.extend({
// See if the store can find stale data. We sometimes prefer to show stale data and // See if the store can find stale data. We sometimes prefer to show stale data and
// refresh it in the background. // refresh it in the background.
findStale(type, findArgs) { findStale(type, findArgs, opts) {
const stale = this.adapterFor(type).findStale(this, type, findArgs); const stale = this.adapterFor(type).findStale(this, type, findArgs, opts);
if (stale.hasResults) { if (stale.hasResults) {
stale.results = this._hydrateFindResults(stale.results, type, findArgs); stale.results = this._hydrateFindResults(stale.results, type, findArgs);
} }
stale.refresh = () => this.find(type, findArgs); stale.refresh = () => this.find(type, findArgs, opts);
return stale; return stale;
}, },
find(type, findArgs) { find(type, findArgs, opts) {
return this.adapterFor(type).find(this, type, findArgs).then((result) => { return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => {
return this._hydrateFindResults(result, type, findArgs); return this._hydrateFindResults(result, type, findArgs, opts);
}); });
}, },
@ -157,6 +157,10 @@ export default Ember.Object.extend({
obj.__type = type; obj.__type = type;
obj.__state = obj.id ? "created" : "new"; obj.__state = obj.id ? "created" : "new";
// TODO: Have injections be automatic
obj.topicTrackingState = this.container.lookup('topic-tracking-state:main');
obj.keyValueStore = this.container.lookup('key-value-store:main');
const klass = this.container.lookupFactory('model:' + type) || RestModel; const klass = this.container.lookupFactory('model:' + type) || RestModel;
const model = klass.create(obj); const model = klass.create(obj);

View File

@ -147,9 +147,6 @@ TopicList.reopenClass({
json.per_page = json.topic_list.per_page; json.per_page = json.topic_list.per_page;
json.topics = topicsFrom(json, store); json.topics = topicsFrom(json, store);
if (json.topic_list.filtered_category) {
json.category = Discourse.Category.create(json.topic_list.filtered_category);
}
return json; return json;
}, },
@ -163,10 +160,9 @@ TopicList.reopenClass({
return this.find(filter); return this.find(filter);
}, },
// Sets `hideCategory` if all topics in the last have a particular category // hide the category when it has no children
hideUniformCategory(list, category) { hideUniformCategory(list, category) {
const hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; }); list.set('hideCategory', category && !category.get("has_children"));
list.set('hideCategory', hideCategory);
} }
}); });

View File

@ -1,4 +1,6 @@
import NotificationLevels from 'discourse/lib/notification-levels'; import NotificationLevels from 'discourse/lib/notification-levels';
import computed from "ember-addons/ember-computed-decorators";
import { on } from "ember-addons/ember-computed-decorators";
function isNew(topic) { function isNew(topic) {
return topic.last_read_post_number === null && return topic.last_read_post_number === null &&
@ -15,24 +17,25 @@ function isUnread(topic) {
const TopicTrackingState = Discourse.Model.extend({ const TopicTrackingState = Discourse.Model.extend({
messageCount: 0, messageCount: 0,
_setup: function() { @on("init")
_setup() {
this.unreadSequence = []; this.unreadSequence = [];
this.newSequence = []; this.newSequence = [];
this.states = {}; this.states = {};
}.on('init'), },
establishChannels() { establishChannels() {
const tracker = this; const tracker = this;
const process = function(data){ const process = data => {
if (data.message_type === "delete") { if (data.message_type === "delete") {
tracker.removeTopic(data.topic_id); tracker.removeTopic(data.topic_id);
tracker.incrementMessageCount(); tracker.incrementMessageCount();
} }
if (data.message_type === "new_topic" || data.message_type === "latest") { if (data.message_type === "new_topic" || data.message_type === "latest") {
const ignored_categories = Discourse.User.currentProp("muted_category_ids"); const muted_category_ids = Discourse.User.currentProp("muted_category_ids");
if(_.include(ignored_categories, data.payload.category_id)){ if (_.include(muted_category_ids, data.payload.category_id)) {
return; return;
} }
} }
@ -45,7 +48,7 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.notify(data); tracker.notify(data);
const old = tracker.states["t" + data.topic_id]; const old = tracker.states["t" + data.topic_id];
if(!_.isEqual(old, data.payload)){ if (!_.isEqual(old, data.payload)) {
tracker.states["t" + data.topic_id] = data.payload; tracker.states["t" + data.topic_id] = data.payload;
tracker.incrementMessageCount(); tracker.incrementMessageCount();
} }
@ -60,20 +63,27 @@ const TopicTrackingState = Discourse.Model.extend({
}, },
updateSeen(topicId, highestSeen) { updateSeen(topicId, highestSeen) {
if(!topicId || !highestSeen) { return; } if (!topicId || !highestSeen) { return; }
const state = this.states["t" + topicId]; const state = this.states["t" + topicId];
if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { if (state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) {
state.last_read_post_number = highestSeen; state.last_read_post_number = highestSeen;
this.incrementMessageCount(); this.incrementMessageCount();
} }
}, },
notify(data){ notify(data) {
if (!this.newIncoming) { return; } if (!this.newIncoming) { return; }
const filter = this.get("filter"); const filter = this.get("filter");
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic" ) { if (filter === Discourse.Utilities.defaultHomepage()) {
const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids");
if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) {
return;
}
}
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") {
this.addIncoming(data.topic_id); this.addIncoming(data.topic_id);
} }
@ -84,7 +94,7 @@ const TopicTrackingState = Discourse.Model.extend({
} }
} }
if(filter === "latest" && data.message_type === "latest") { if (filter === "latest" && data.message_type === "latest") {
this.addIncoming(data.topic_id); this.addIncoming(data.topic_id);
} }
@ -92,12 +102,12 @@ const TopicTrackingState = Discourse.Model.extend({
}, },
addIncoming(topicId) { addIncoming(topicId) {
if(this.newIncoming.indexOf(topicId) === -1){ if (this.newIncoming.indexOf(topicId) === -1) {
this.newIncoming.push(topicId); this.newIncoming.push(topicId);
} }
}, },
resetTracking(){ resetTracking() {
this.newIncoming = []; this.newIncoming = [];
this.set("incomingCount", 0); this.set("incomingCount", 0);
}, },
@ -109,10 +119,10 @@ const TopicTrackingState = Discourse.Model.extend({
this.set("incomingCount", 0); this.set("incomingCount", 0);
}, },
hasIncoming: function(){ @computed("incomingCount")
const count = this.get('incomingCount'); hasIncoming(incomingCount) {
return count && count > 0; return incomingCount && incomingCount > 0;
}.property('incomingCount'), },
removeTopic(topic_id) { removeTopic(topic_id) {
delete this.states["t" + topic_id]; delete this.states["t" + topic_id];
@ -124,7 +134,7 @@ const TopicTrackingState = Discourse.Model.extend({
if (Em.isEmpty(topics)) { return; } if (Em.isEmpty(topics)) { return; }
const states = this.states; const states = this.states;
topics.forEach(function(t) { topics.forEach(t => {
const state = states['t' + t.get('id')]; const state = states['t' + t.get('id')];
if (state) { if (state) {
@ -135,9 +145,7 @@ const TopicTrackingState = Discourse.Model.extend({
unread = postsCount - state.last_read_post_number; unread = postsCount - state.last_read_post_number;
if (newPosts < 0) { newPosts = 0; } if (newPosts < 0) { newPosts = 0; }
if (!state.last_read_post_number) { if (!state.last_read_post_number) { unread = 0; }
unread = 0;
}
if (unread < 0) { unread = 0; } if (unread < 0) { unread = 0; }
t.setProperties({ t.setProperties({
@ -166,8 +174,8 @@ const TopicTrackingState = Discourse.Model.extend({
if (filter === "new") { if (filter === "new") {
list.topics.splice(i, 1); list.topics.splice(i, 1);
} else { } else {
list.topics[i].unseen = false; list.topics[i].set('unseen', false);
list.topics[i].dont_sync = true; list.topics[i].set('dont_sync', true);
} }
} }
} }
@ -198,14 +206,12 @@ const TopicTrackingState = Discourse.Model.extend({
}); });
// Correct missing states, safeguard in case message bus is corrupt // Correct missing states, safeguard in case message bus is corrupt
if((filter === "new" || filter === "unread") && !list.more_topics_url){ if ((filter === "new" || filter === "unread") && !list.more_topics_url) {
const ids = {}; const ids = {};
list.topics.forEach(function(r){ list.topics.forEach(r => ids["t" + r.id] = true);
ids["t" + r.id] = true;
});
_.each(tracker.states, function(v, k){ _.each(tracker.states, (v, k) => {
// we are good if we are on the list // we are good if we are on the list
if (ids[k]) { return; } if (ids[k]) { return; }
@ -229,31 +235,26 @@ const TopicTrackingState = Discourse.Model.extend({
this.set("messageCount", this.get("messageCount") + 1); this.set("messageCount", this.get("messageCount") + 1);
}, },
countNew(category_id){ countNew(category_id) {
return _.chain(this.states) return _.chain(this.states)
.where(isNew) .where(isNew)
.where(function(topic){ return topic.category_id === category_id || !category_id;}) .where(topic => topic.category_id === category_id || !category_id)
.value() .value()
.length; .length;
}, },
tooManyTracked() {
return this.initialStatesLength >= Discourse.SiteSettings.max_tracked_new_unread;
},
resetNew() { resetNew() {
const self = this; Object.keys(this.states).forEach(id => {
Object.keys(this.states).forEach(function (id) { if (this.states[id].last_read_post_number === null) {
if (self.states[id].last_read_post_number === null) { delete this.states[id];
delete self.states[id];
} }
}); });
}, },
countUnread(category_id){ countUnread(category_id) {
return _.chain(this.states) return _.chain(this.states)
.where(isUnread) .where(isUnread)
.where(function(topic){ return topic.category_id === category_id || !category_id;}) .where(topic => topic.category_id === category_id || !category_id)
.value() .value()
.length; .length;
}, },
@ -269,54 +270,50 @@ const TopicTrackingState = Discourse.Model.extend({
return sum; return sum;
}, },
lookupCount(name, category){ lookupCount(name, category) {
if (name === "latest") { if (name === "latest") {
return this.lookupCount("new", category) + return this.lookupCount("new", category) +
this.lookupCount("unread", category); this.lookupCount("unread", category);
} }
let categoryName = category ? Em.get(category, "name") : null; let categoryName = category ? Em.get(category, "name") : null;
if(name === "new") { if (name === "new") {
return this.countNew(categoryName); return this.countNew(categoryName);
} else if(name === "unread") { } else if (name === "unread") {
return this.countUnread(categoryName); return this.countUnread(categoryName);
} else { } else {
categoryName = name.split("/")[1]; categoryName = name.split("/")[1];
if(categoryName) { if (categoryName) {
return this.countCategory(categoryName); return this.countCategory(categoryName);
} }
} }
}, },
loadStates(data) { loadStates(data) {
// not exposed
const states = this.states; const states = this.states;
if (data) {
if(data) { _.each(data,topic => states["t" + topic.topic_id] = topic);
_.each(data,function(topic){
states["t" + topic.topic_id] = topic;
});
} }
} }
}); });
TopicTrackingState.reopenClass({ TopicTrackingState.reopenClass({
createFromStates(data) {
createFromStates(data) {
// TODO: This should be a model that does injection automatically // TODO: This should be a model that does injection automatically
const container = Discourse.__container__, const container = Discourse.__container__,
messageBus = container.lookup('message-bus:main'), messageBus = container.lookup('message-bus:main'),
currentUser = container.lookup('current-user:main'), currentUser = container.lookup('current-user:main'),
instance = Discourse.TopicTrackingState.create({ messageBus, currentUser }); instance = TopicTrackingState.create({ messageBus, currentUser });
instance.loadStates(data); instance.loadStates(data);
instance.initialStatesLength = data && data.length; instance.initialStatesLength = data && data.length;
instance.establishChannels(); instance.establishChannels();
return instance; return instance;
}, },
current(){
current() {
if (!this.tracker) { if (!this.tracker) {
const data = PreloadStore.get('topicTrackingStates'); const data = PreloadStore.get('topicTrackingStates');
this.tracker = this.createFromStates(data); this.tracker = this.createFromStates(data);

View File

@ -1,5 +1,7 @@
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import { url } from 'discourse/lib/computed'; import { url } from 'discourse/lib/computed';
import { on } from 'ember-addons/ember-computed-decorators';
import computed from 'ember-addons/ember-computed-decorators';
const UserActionTypes = { const UserActionTypes = {
likes_given: 1, likes_given: 1,
@ -17,21 +19,22 @@ const UserActionTypes = {
}; };
const InvertedActionTypes = {}; const InvertedActionTypes = {};
_.each(UserActionTypes, function (k, v) { _.each(UserActionTypes, (k, v) => {
InvertedActionTypes[k] = v; InvertedActionTypes[k] = v;
}); });
const UserAction = RestModel.extend({ const UserAction = RestModel.extend({
_attachCategory: function() { @on("init")
_attachCategory() {
const categoryId = this.get('category_id'); const categoryId = this.get('category_id');
if (categoryId) { if (categoryId) {
this.set('category', Discourse.Category.findById(categoryId)); this.set('category', Discourse.Category.findById(categoryId));
} }
}.on('init'), },
descriptionKey: function() { @computed("action_type")
const action = this.get('action_type'); descriptionKey(action) {
if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) { if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) {
if (this.get('isPM')) { if (this.get('isPM')) {
return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user'; return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user';
@ -59,34 +62,39 @@ const UserAction = RestModel.extend({
return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user'; return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user';
} }
} }
}.property('action_type'), },
sameUser: function() { @computed("username")
return this.get('username') === Discourse.User.currentProp('username'); sameUser(username) {
}.property('username'), return username === Discourse.User.currentProp('username');
},
targetUser: function() { @computed("target_username")
return this.get('target_username') === Discourse.User.currentProp('username'); targetUser(targetUsername) {
}.property('target_username'), return targetUsername === Discourse.User.currentProp('username');
},
presentName: Em.computed.any('name', 'username'), presentName: Em.computed.any('name', 'username'),
targetDisplayName: Em.computed.any('target_name', 'target_username'), targetDisplayName: Em.computed.any('target_name', 'target_username'),
actingDisplayName: Em.computed.any('acting_name', 'acting_username'), actingDisplayName: Em.computed.any('acting_name', 'acting_username'),
targetUserUrl: url('target_username', '/users/%@'), targetUserUrl: url('target_username', '/users/%@'),
usernameLower: function() { @computed("username")
return this.get('username').toLowerCase(); usernameLower(username) {
}.property('username'), return username.toLowerCase();
},
userUrl: url('usernameLower', '/users/%@'), userUrl: url('usernameLower', '/users/%@'),
postUrl: function() { @computed()
postUrl() {
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number')); return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
}.property(), },
replyUrl: function() { @computed()
replyUrl() {
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number')); return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
}.property(), },
replyType: Em.computed.equal('action_type', UserActionTypes.replies), replyType: Em.computed.equal('action_type', UserActionTypes.replies),
postType: Em.computed.equal('action_type', UserActionTypes.posts), postType: Em.computed.equal('action_type', UserActionTypes.posts),
@ -99,7 +107,7 @@ const UserAction = RestModel.extend({
postReplyType: Em.computed.or('postType', 'replyType'), postReplyType: Em.computed.or('postType', 'replyType'),
removableBookmark: Em.computed.and('bookmarkType', 'sameUser'), removableBookmark: Em.computed.and('bookmarkType', 'sameUser'),
addChild: function(action) { addChild(action) {
let groups = this.get("childGroups"); let groups = this.get("childGroups");
if (!groups) { if (!groups) {
groups = { groups = {
@ -143,22 +151,21 @@ const UserAction = RestModel.extend({
"childGroups.edits.items", "childGroups.edits.items.@each", "childGroups.edits.items", "childGroups.edits.items.@each",
"childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"), "childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"),
switchToActing: function() { switchToActing() {
this.setProperties({ this.setProperties({
username: this.get('acting_username'), username: this.get('acting_username'),
uploaded_avatar_id: this.get('acting_uploaded_avatar_id'),
name: this.get('actingDisplayName') name: this.get('actingDisplayName')
}); });
} }
}); });
UserAction.reopenClass({ UserAction.reopenClass({
collapseStream: function(stream) { collapseStream(stream) {
const uniq = {}; const uniq = {};
const collapsed = []; const collapsed = [];
let pos = 0; let pos = 0;
stream.forEach(function(item) { stream.forEach(item => {
const key = "" + item.topic_id + "-" + item.post_number; const key = "" + item.topic_id + "-" + item.post_number;
const found = uniq[key]; const found = uniq[key];
if (found === void 0) { if (found === void 0) {

View File

@ -1,11 +1,11 @@
import { url } from 'discourse/lib/computed'; import { url } from 'discourse/lib/computed';
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import avatarTemplate from 'discourse/lib/avatar-template';
import UserStream from 'discourse/models/user-stream'; import UserStream from 'discourse/models/user-stream';
import UserPostsStream from 'discourse/models/user-posts-stream'; import UserPostsStream from 'discourse/models/user-posts-stream';
import Singleton from 'discourse/mixins/singleton'; import Singleton from 'discourse/mixins/singleton';
import { longDate } from 'discourse/lib/formatter'; import { longDate } from 'discourse/lib/formatter';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import { observes } from 'ember-addons/ember-computed-decorators';
import Badge from 'discourse/models/badge'; import Badge from 'discourse/models/badge';
import UserBadge from 'discourse/models/user-badge'; import UserBadge from 'discourse/models/user-badge';
@ -18,13 +18,15 @@ const User = RestModel.extend({
hasNotPosted: Em.computed.not("hasPosted"), hasNotPosted: Em.computed.not("hasPosted"),
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"), canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
stream: function() { @computed()
stream() {
return UserStream.create({ user: this }); return UserStream.create({ user: this });
}.property(), },
postsStream: function() { @computed()
postsStream() {
return UserPostsStream.create({ user: this }); return UserPostsStream.create({ user: this });
}.property(), },
staff: Em.computed.or('admin', 'moderator'), staff: Em.computed.or('admin', 'moderator'),
@ -32,27 +34,22 @@ const User = RestModel.extend({
return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'}); return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'});
}, },
searchContext: function() { @computed("username_lower")
searchContext(username) {
return { return {
type: 'user', type: 'user',
id: this.get('username_lower'), id: username,
user: this user: this
}; };
}.property('username_lower'), },
/** @computed("username", "name")
This user's display name. Returns the name if possible, otherwise returns the displayName(username, name) {
username. if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
return name;
@property displayName
@type {String}
**/
displayName: function() {
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) {
return this.get('name');
} }
return this.get('username'); return username;
}.property('username', 'name'), },
@computed('profile_background') @computed('profile_background')
profileBackground(bgUrl) { profileBackground(bgUrl) {
@ -60,44 +57,23 @@ const User = RestModel.extend({
return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe();
}, },
/** @computed()
Path to this user. path() {
@property path
@type {String}
**/
path: function(){
return Discourse.getURL('/users/' + this.get('username_lower'));
// no need to observe, requires a hard refresh to update // no need to observe, requires a hard refresh to update
}.property(), return Discourse.getURL(`/users/${this.get('username_lower')}`);
},
/**
Path to this user's administration
@property adminPath
@type {String}
**/
adminPath: url('username_lower', "/admin/users/%@"), adminPath: url('username_lower', "/admin/users/%@"),
/** @computed("username")
This user's username in lowercase. username_lower(username) {
return username.toLowerCase();
},
@property username_lower @computed("trust_level")
@type {String} trustLevel(trustLevel) {
**/ return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10));
username_lower: function() { },
return this.get('username').toLowerCase();
}.property('username'),
/**
This user's trust level.
@property trustLevel
@type {Integer}
**/
trustLevel: function() {
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10));
}.property('trust_level'),
isBasic: Em.computed.equal('trust_level', 0), isBasic: Em.computed.equal('trust_level', 0),
isLeader: Em.computed.equal('trust_level', 3), isLeader: Em.computed.equal('trust_level', 3),
@ -106,61 +82,36 @@ const User = RestModel.extend({
isSuspended: Em.computed.equal('suspended', true), isSuspended: Em.computed.equal('suspended', true),
suspended: function() { @computed("suspended_till")
return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter(); suspended(suspendedTill) {
}.property('suspended_till'), return suspendedTill && moment(suspendedTill).isAfter();
},
suspendedTillDate: function() { @computed("suspended_till")
return longDate(this.get('suspended_till')); suspendedTillDate(suspendedTill) {
}.property('suspended_till'), return longDate(suspendedTill);
},
/** changeUsername(new_username) {
Changes this user's username. return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, {
@method changeUsername
@param {String} newUsername The user's new username
@returns Result of ajax call
**/
changeUsername: function(newUsername) {
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", {
type: 'PUT', type: 'PUT',
data: { new_username: newUsername } data: { new_username }
}); });
}, },
/** changeEmail(email) {
Changes this user's email address. return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, {
@method changeEmail
@param {String} email The user's new email address\
@returns Result of ajax call
**/
changeEmail: function(email) {
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", {
type: 'PUT', type: 'PUT',
data: { email: email } data: { email }
}); });
}, },
/** copy() {
Returns a copy of this user.
@method copy
@returns {User}
**/
copy: function() {
return Discourse.User.create(this.getProperties(Ember.keys(this))); return Discourse.User.create(this.getProperties(Ember.keys(this)));
}, },
/** save() {
Save's this user's properties over AJAX via a PUT request. const data = this.getProperties(
@method save
@returns {Promise} the result of the operation
**/
save: function() {
const self = this,
data = this.getProperties(
'auto_track_topics_after_msecs', 'auto_track_topics_after_msecs',
'bio_raw', 'bio_raw',
'website', 'website',
@ -185,10 +136,10 @@ const User = RestModel.extend({
'card_background' 'card_background'
); );
['muted','watched','tracked'].forEach(function(s){ ['muted','watched','tracked'].forEach(s => {
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')}); let cats = this.get(s + 'Categories').map(c => c.get('id'));
// HACK: denote lack of categories // HACK: denote lack of categories
if(cats.length === 0) { cats = [-1]; } if (cats.length === 0) { cats = [-1]; }
data[s + '_category_ids'] = cats; data[s + '_category_ids'] = cats;
}); });
@ -198,26 +149,19 @@ const User = RestModel.extend({
// TODO: We can remove this when migrated fully to rest model. // TODO: We can remove this when migrated fully to rest model.
this.set('isSaving', true); this.set('isSaving', true);
return Discourse.ajax("/users/" + this.get('username_lower'), { return Discourse.ajax(`/users/${this.get('username_lower')}`, {
data: data, data: data,
type: 'PUT' type: 'PUT'
}).then(function(result) { }).then(result => {
self.set('bio_excerpt', result.user.bio_excerpt); this.set('bio_excerpt', result.user.bio_excerpt);
const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
Discourse.User.current().setProperties(userProps); Discourse.User.current().setProperties(userProps);
}).finally(() => { }).finally(() => {
this.set('isSaving', false); this.set('isSaving', false);
}); });
}, },
/** changePassword() {
Changes the password and calls the callback function on AJAX.complete.
@method changePassword
@returns {Promise} the result of the change password operation
**/
changePassword: function() {
return Discourse.ajax("/session/forgot_password", { return Discourse.ajax("/session/forgot_password", {
dataType: 'json', dataType: 'json',
data: { login: this.get('username') }, data: { login: this.get('username') },
@ -225,73 +169,63 @@ const User = RestModel.extend({
}); });
}, },
/** loadUserAction(id) {
Loads a single user action by id. const stream = this.get('stream');
return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {
@method loadUserAction
@param {Integer} id The id of the user action being loaded
@returns A stream of the user's actions containing the action of id
**/
loadUserAction: function(id) {
var self = this,
stream = this.get('stream');
return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) {
if (result && result.user_action) { if (result && result.user_action) {
var ua = result.user_action; const ua = result.user_action;
if ((self.get('stream.filter') || ua.action_type) !== ua.action_type) return; if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return;
if (!self.get('stream.filter') && !self.inAllStream(ua)) return; if (!this.get('stream.filter') && !this.inAllStream(ua)) return;
var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]); const action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]);
stream.set('itemsLoaded', stream.get('itemsLoaded') + 1); stream.set('itemsLoaded', stream.get('itemsLoaded') + 1);
stream.get('content').insertAt(0, action[0]); stream.get('content').insertAt(0, action[0]);
} }
}); });
}, },
inAllStream: function(ua) { inAllStream(ua) {
return ua.action_type === Discourse.UserAction.TYPES.posts || return ua.action_type === Discourse.UserAction.TYPES.posts ||
ua.action_type === Discourse.UserAction.TYPES.topics; ua.action_type === Discourse.UserAction.TYPES.topics;
}, },
// The user's stat count, excluding PMs. // The user's stat count, excluding PMs.
statsCountNonPM: function() { @computed("statsExcludingPms.@each.count")
var self = this; statsCountNonPM() {
if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0; if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0;
var count = 0; let count = 0;
_.each(this.get('statsExcludingPms'), function(val) { _.each(this.get('statsExcludingPms'), val => {
if (self.inAllStream(val)){ if (this.inAllStream(val)) {
count += val.count; count += val.count;
} }
}); });
return count; return count;
}.property('statsExcludingPms.@each.count'), },
// The user's stats, excluding PMs. // The user's stats, excluding PMs.
statsExcludingPms: function() { @computed("stats.@each.isPM")
statsExcludingPms() {
if (Ember.isEmpty(this.get('stats'))) return []; if (Ember.isEmpty(this.get('stats'))) return [];
return this.get('stats').rejectProperty('isPM'); return this.get('stats').rejectProperty('isPM');
}.property('stats.@each.isPM'), },
findDetails: function(options) { findDetails(options) {
var user = this; const user = this;
return PreloadStore.getAndRemove("user_" + user.get('username'), function() { return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => {
return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options}); return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options });
}).then(function (json) { }).then(json => {
if (!Em.isEmpty(json.user.stats)) { if (!Em.isEmpty(json.user.stats)) {
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) { json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => {
if (s.count) s.count = parseInt(s.count, 10); if (s.count) s.count = parseInt(s.count, 10);
return Discourse.UserActionStat.create(s); return Discourse.UserActionStat.create(s);
})); }));
} }
if (!Em.isEmpty(json.user.custom_groups)) { if (!Em.isEmpty(json.user.custom_groups)) {
json.user.custom_groups = json.user.custom_groups.map(function (g) { json.user.custom_groups = json.user.custom_groups.map(g => Discourse.Group.create(g));
return Discourse.Group.create(g);
});
} }
if (json.user.invited_by) { if (json.user.invited_by) {
@ -300,12 +234,10 @@ const User = RestModel.extend({
if (!Em.isEmpty(json.user.featured_user_badge_ids)) { if (!Em.isEmpty(json.user.featured_user_badge_ids)) {
const userBadgesMap = {}; const userBadgesMap = {};
UserBadge.createFromJson(json).forEach(function(userBadge) { UserBadge.createFromJson(json).forEach(userBadge => {
userBadgesMap[ userBadge.get('id') ] = userBadge; userBadgesMap[ userBadge.get('id') ] = userBadge;
}); });
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]);
return userBadgesMap[id];
});
} }
if (json.user.card_badge) { if (json.user.card_badge) {
@ -317,81 +249,62 @@ const User = RestModel.extend({
}); });
}, },
findStaffInfo: function() { findStaffInfo() {
if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); } if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); }
var self = this; return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => {
return Discourse.ajax("/users/" + this.get("username_lower") + "/staff-info.json").then(function(info) { this.setProperties(info);
self.setProperties(info);
}); });
}, },
avatarTemplate: function() { pickAvatar(upload_id, type, avatar_template) {
return avatarTemplate(this.get('username'), this.get('uploaded_avatar_id')); return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
}.property('uploaded_avatar_id', 'username'),
/*
Change avatar selection
*/
pickAvatar: function(uploadId) {
var self = this;
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", {
type: 'PUT', type: 'PUT',
data: { upload_id: uploadId } data: { upload_id, type }
}).then(function(){ }).then(() => this.setProperties({
self.set('uploaded_avatar_id', uploadId); avatar_template,
}); uploaded_avatar_id: upload_id
}));
}, },
/** isAllowedToUploadAFile(type) {
Determines whether the current user is allowed to upload a file.
@method isAllowedToUploadAFile
@param {String} type The type of the upload (image, attachment)
@returns true if the current user is allowed to upload a file
**/
isAllowedToUploadAFile: function(type) {
return this.get('staff') || return this.get('staff') ||
this.get('trust_level') > 0 || this.get('trust_level') > 0 ||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0; Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
}, },
/** createInvite(email, group_names) {
Invite a user to the site
@method createInvite
@param {String} email The email address of the user to invite to the site
@returns {Promise} the result of the server call
**/
createInvite: function(email, groupNames) {
return Discourse.ajax('/invites', { return Discourse.ajax('/invites', {
type: 'POST', type: 'POST',
data: {email: email, group_names: groupNames} data: { email, group_names }
}); });
}, },
generateInviteLink: function(email, groupNames, topicId) { generateInviteLink(email, group_names, topic_id) {
return Discourse.ajax('/invites/link', { return Discourse.ajax('/invites/link', {
type: 'POST', type: 'POST',
data: {email: email, group_names: groupNames, topic_id: topicId} data: { email, group_names, topic_id }
}); });
}, },
updateMutedCategories: function() { @observes("muted_category_ids")
updateMutedCategories() {
this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids));
}.observes("muted_category_ids"), },
updateTrackedCategories: function() { @observes("tracked_category_ids")
updateTrackedCategories() {
this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids)); this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids));
}.observes("tracked_category_ids"), },
updateWatchedCategories: function() { @observes("watched_category_ids")
updateWatchedCategories() {
this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids));
}.observes("watched_category_ids"), },
canDeleteAccount: function() { @computed("can_delete_account", "reply_count", "topic_count")
return !Discourse.SiteSettings.enable_sso && this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1; canDeleteAccount(canDeleteAccount, replyCount, topicCount) {
}.property('can_delete_account', 'reply_count', 'topic_count'), return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1;
},
"delete": function() { "delete": function() {
if (this.get('can_delete_account')) { if (this.get('can_delete_account')) {
@ -404,27 +317,26 @@ const User = RestModel.extend({
} }
}, },
dismissBanner: function (bannerKey) { dismissBanner(bannerKey) {
this.set("dismissed_banner_key", bannerKey); this.set("dismissed_banner_key", bannerKey);
Discourse.ajax("/users/" + this.get('username'), { Discourse.ajax(`/users/${this.get('username')}`, {
type: 'PUT', type: 'PUT',
data: { dismissed_banner_key: bannerKey } data: { dismissed_banner_key: bannerKey }
}); });
}, },
checkEmail: function () { checkEmail() {
var self = this; return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, {
return Discourse.ajax("/users/" + this.get("username_lower") + "/emails.json", {
type: "PUT", type: "PUT",
data: { context: window.location.pathname } data: { context: window.location.pathname }
}).then(function (result) { }).then(result => {
if (result) { if (result) {
self.setProperties({ this.setProperties({
email: result.email, email: result.email,
associated_accounts: result.associated_accounts associated_accounts: result.associated_accounts
}); });
} }
}, function () {}); });
} }
}); });
@ -432,14 +344,14 @@ const User = RestModel.extend({
User.reopenClass(Singleton, { User.reopenClass(Singleton, {
// Find a `Discourse.User` for a given username. // Find a `Discourse.User` for a given username.
findByUsername: function(username, options) { findByUsername(username, options) {
const user = User.create({username: username}); const user = User.create({username: username});
return user.findDetails(options); return user.findDetails(options);
}, },
// TODO: Use app.register and junk Singleton // TODO: Use app.register and junk Singleton
createCurrent: function() { createCurrent() {
var userJson = PreloadStore.get('currentUser'); const userJson = PreloadStore.get('currentUser');
if (userJson) { if (userJson) {
const store = Discourse.__container__.lookup('store:main'); const store = Discourse.__container__.lookup('store:main');
return store.createRecord('user', userJson); return store.createRecord('user', userJson);
@ -447,56 +359,38 @@ User.reopenClass(Singleton, {
return null; return null;
}, },
/** checkUsername(username, email, for_user_id) {
Checks if given username is valid for this email address
@method checkUsername
@param {String} username A username to check
@param {String} email An email address to check
@param {Number} forUserId user id - provide when changing username
**/
checkUsername: function(username, email, forUserId) {
return Discourse.ajax('/users/check_username', { return Discourse.ajax('/users/check_username', {
data: { username: username, email: email, for_user_id: forUserId } data: { username, email, for_user_id }
}); });
}, },
/** groupStats(stats) {
Groups the user's statistics const responses = Discourse.UserActionStat.create({
@method groupStats
@param {Array} stats Given stats
@returns {Object}
**/
groupStats: function(stats) {
var responses = Discourse.UserActionStat.create({
count: 0, count: 0,
action_type: Discourse.UserAction.TYPES.replies action_type: Discourse.UserAction.TYPES.replies
}); });
stats.filterProperty('isResponse').forEach(function (stat) { stats.filterProperty('isResponse').forEach(stat => {
responses.set('count', responses.get('count') + stat.get('count')); responses.set('count', responses.get('count') + stat.get('count'));
}); });
var result = Em.A(); const result = Em.A();
result.pushObjects(stats.rejectProperty('isResponse')); result.pushObjects(stats.rejectProperty('isResponse'));
var insertAt = 0; let insertAt = 0;
result.forEach(function(item, index){ result.forEach((item, index) => {
if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){ if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) {
insertAt = index + 1; insertAt = index + 1;
} }
}); });
if(responses.count > 0) { if (responses.count > 0) {
result.insertAt(insertAt, responses); result.insertAt(insertAt, responses);
} }
return(result); return result;
}, },
/** createAccount(attrs) {
Creates a new account
**/
createAccount: function(attrs) {
return Discourse.ajax("/users", { return Discourse.ajax("/users", {
data: { data: {
name: attrs.accountName, name: attrs.accountName,

View File

@ -3,6 +3,7 @@ import buildTopicRoute from 'discourse/routes/build-topic-route';
import DiscoverySortableController from 'discourse/controllers/discovery-sortable'; import DiscoverySortableController from 'discourse/controllers/discovery-sortable';
export default { export default {
after: 'inject-discourse-objects',
name: 'dynamic-route-builders', name: 'dynamic-route-builders',
initialize(container, app) { initialize(container, app) {

View File

@ -5,6 +5,7 @@ import Store from 'discourse/models/store';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import DiscourseLocation from 'discourse/lib/discourse-location'; import DiscourseLocation from 'discourse/lib/discourse-location';
import SearchService from 'discourse/services/search'; import SearchService from 'discourse/services/search';
import TopicTrackingState from 'discourse/models/topic-tracking-state';
function inject() { function inject() {
const app = arguments[0], const app = arguments[0],
@ -30,6 +31,12 @@ export default {
app.register('store:main', Store); app.register('store:main', Store);
inject(app, 'store', 'route', 'controller'); inject(app, 'store', 'route', 'controller');
app.register('message-bus:main', window.MessageBus, { instantiate: false });
injectAll(app, 'messageBus');
app.register('topic-tracking-state:main', TopicTrackingState.current(), { instantiate: false });
injectAll(app, 'topicTrackingState');
const site = Discourse.Site.current(); const site = Discourse.Site.current();
app.register('site:main', site, { instantiate: false }); app.register('site:main', site, { instantiate: false });
injectAll(app, 'site'); injectAll(app, 'site');
@ -46,9 +53,6 @@ export default {
app.register('current-user:main', Discourse.User.current(), { instantiate: false }); app.register('current-user:main', Discourse.User.current(), { instantiate: false });
inject(app, 'currentUser', 'component', 'route', 'controller'); inject(app, 'currentUser', 'component', 'route', 'controller');
app.register('message-bus:main', window.MessageBus, { instantiate: false });
injectAll(app, 'messageBus');
app.register('location:discourse-location', DiscourseLocation); app.register('location:discourse-location', DiscourseLocation);
const keyValueStore = new KeyValueStore("discourse_"); const keyValueStore = new KeyValueStore("discourse_");

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