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
## 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
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
*Thanks for contributing!*

View File

@ -63,7 +63,8 @@ gem 'email_reply_parser'
# note: for image_optim to correctly work you need to follow
# 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 'mustache'
gem 'nokogiri'
@ -90,6 +91,7 @@ gem 'rinku'
gem 'sanitize'
gem 'sass'
gem 'sidekiq'
gem 'sidekiq-statistic'
# for sidekiq web
gem 'sinatra', require: false

View File

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

View File

@ -22,7 +22,7 @@ export default Ember.Component.extend({
this.set("show", true);
if (!this.get("location")) {
Discourse.ajax("/admin/users/ip-info.json", {
Discourse.ajax("/admin/users/ip-info", {
data: { ip: this.get("ip") }
}).then(function (location) {
self.set("location", Em.Object.create(location));
@ -38,7 +38,7 @@ export default Ember.Component.extend({
"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);
});

View File

@ -39,7 +39,7 @@ export default Ember.Controller.extend({
if (this.get("showingLast")) { return; }
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);
@ -50,7 +50,7 @@ export default Ember.Controller.extend({
if (this.get("showingFirst")) { return; }
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);

View File

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

View File

@ -8,9 +8,9 @@
<div class="toggle">
<label>{{i18n 'admin.email.format'}}</label>
{{#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}}
<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}}
</div>
</div>

View File

@ -13,9 +13,9 @@
<div>
<label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label>
<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}}
<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 class="ac-wrap clearfix">
{{#each model.members as |member|}}
@ -28,7 +28,7 @@
<div>
<label for="user-selector">{{i18n 'admin.groups.add_members'}}</label>
{{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>
{{/unless}}
{{/if}}

View File

@ -1,10 +1,10 @@
<div>
<ul class="nav nav-pills">
<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 {{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>
</ul>
<div class="modal-body">

View File

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

View File

@ -19,7 +19,7 @@ export default Ember.Component.extend(StringBuffer, {
const renderActionIf = function(property, dataAttribute, text) {
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

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({
autoCloseValid: false,
limited: false,
autoCloseValid: false,
autoCloseUnits: function() {
var key = this.get("limited") ? "composer.auto_close.limited.units"
: "composer.auto_close.all.units";
@computed("limited")
autoCloseUnits(limited) {
const key = limited ? "composer.auto_close.limited.units" : "composer.auto_close.all.units";
return I18n.t(key);
}.property("limited"),
},
autoCloseExamples: function() {
var key = this.get("limited") ? "composer.auto_close.limited.examples"
: "composer.auto_close.all.examples";
@computed("limited")
autoCloseExamples(limited) {
const key = limited ? "composer.auto_close.limited.examples" : "composer.auto_close.all.examples";
return I18n.t(key);
}.property("limited"),
},
_updateAutoCloseValid: function() {
var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited"));
@observes("autoCloseTime", "limited")
_updateAutoCloseValid() {
const limited = this.get("limited"),
autoCloseTime = this.get("autoCloseTime"),
isValid = this._isAutoCloseValid(autoCloseTime, limited);
this.set("autoCloseValid", isValid);
}.observes("autoCloseTime", "limited"),
},
_isAutoCloseValid: function(autoCloseTime, limited) {
var t = (autoCloseTime || "").toString().trim();
_isAutoCloseValid(autoCloseTime, limited) {
const t = (autoCloseTime || "").toString().trim();
if (t.length === 0) {
// "empty" is always valid
return true;

View File

@ -1,7 +1,12 @@
import { on } from "ember-addons/ember-computed-decorators";
export default Ember.TextField.extend({
becomeFocused: function() {
var input = this.get("element");
@on("didInsertElement")
becomeFocused() {
const input = this.get("element");
input.focus();
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";
export default Em.Component.extend(UploadMixin, {
@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, {
tagName: "span",
imageIsNotASquare: false,
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
}.property("uploading"),
@computed("uploading")
uploadButtonText(uploading) {
return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
},
uploadDone(upload) {
this.setProperties({
imageIsNotASquare: upload.width !== upload.height,
uploadedAvatarTemplate: upload.url,
custom_avatar_upload_id: upload.id,
uploadedAvatarId: upload.id,
});
this.sendAction("done");
},
data: function() {
return { user_id: this.get("user_id") };
}.property("user_id")
@computed("user_id")
data(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 { 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({
classNames: ['combobox category-combobox'],
@ -8,46 +10,34 @@ export default ComboboxView.extend({
valueBinding: Ember.Binding.oneWay('source'),
castInteger: true,
content: function() {
let scopedCategoryId = this.get('scopedCategoryId');
@computed("scopedCategoryId", "categories")
content(scopedCategoryId, categories) {
// Always scope to the parent of a category, if present
if (scopedCategoryId) {
const scopedCat = Discourse.Category.findById(scopedCategoryId);
scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id');
}
return this.get('categories').filter(function(c) {
if (scopedCategoryId && (c.get('id') !== scopedCategoryId) && (c.get('parent_category_id') !== scopedCategoryId)) {
return false;
}
return c.get('permission') === Discourse.PermissionType.FULL && !c.get('isUncategorizedCategory');
return categories.filter(c => {
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory')) { return false; }
return c.get('permission') === Discourse.PermissionType.FULL;
});
}.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')) {
this.set('automatic', true);
}
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() {
@computed("rootNone")
none(rootNone) {
if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) {
if (this.get('rootNone')) {
if (rootNone) {
return "category.none";
} else {
return Discourse.Category.findUncategorized();
@ -55,10 +45,9 @@ export default ComboboxView.extend({
} else {
return 'category.choose';
}
}.property(),
},
comboTemplate(item) {
let category;
// 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 += " <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');
// TODO wtf how can this be null?;
if (description && description !== 'null') {
result += '<div class="category-desc">' +
description.substr(0,200) +
(description.length > 200 ? '&hellip;' : '') +
'</div>';
result += `<div class="category-desc">${description.substr(0, 200)}${description.length > 200 ? '&hellip;' : ''}</div>`;
}
return result;
}

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import computed from 'ember-addons/ember-computed-decorators';
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({
tagName: 'a',
classNames: ['d-link'],
attributeBindings: ['translatedTitle:title', 'translatedTitle:aria-title', 'href'],
@computed('path')
@ -14,7 +15,13 @@ export default Ember.Component.extend({
if (route) {
const router = this.container.lookup('router:main');
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);
},
click() {
click(e) {
const action = this.get('action');
if (action) {
this.sendAction('action');
return false;
}
const href = this.get('href');
if (href) {
DiscourseURL.routeTo(href);
return false;
}
return false;
return interceptClick(e);
},
render(buffer) {
@ -55,7 +58,8 @@ export default Ember.Component.extend({
if (label) {
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("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>");
buffer.push(this.get('text'));
buffer.push("</button>");
buffer.push(`<button class='btn standard dropdown-toggle ${this.get('buttonExtraClasses')}' data-toggle='dropdown'>${this.get('text')}</button>`);
buffer.push("<ul class='dropdown-menu'>");
const contents = this.get('dropDownContent');

View File

@ -1,6 +1,7 @@
import DiscourseURL from 'discourse/lib/url';
import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
export default buildCategoryPanel('general', {
foregroundColors: ['FFFFFF', '000000'],
@ -31,7 +32,7 @@ export default buildCategoryPanel('general', {
categoryBadgePreview: function() {
const category = this.get('category');
const c = Discourse.Category.create({
const c = Category.create({
name: category.get('categoryName'),
color: category.get('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
subCategories: function() {
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'),
showDescription: function() {

View File

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

View File

@ -2,6 +2,12 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
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()
showKeyboardShortcuts() {
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');
},
_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()
categories() {
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({
tagName: 'li',
classNameBindings: [':header-dropdown-toggle', 'active'],
@computed('showUser')
href(showUser) {
return showUser ? this.currentUser.get('path') : '';
},
active: Ember.computed.alias('toggleVisible'),
actions: {
toggle() {
if (Discourse.Mobile.mobileView && this.get('mobileAction')) {
this.sendAction('mobileAction');
return;
}
if (this.siteSettings.login_required && !this.currentUser) {
this.sendAction('loginAction');
} else {

View File

@ -1,4 +1,5 @@
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
import { headerHeight } from 'discourse/views/header';
const PANEL_BODY_MARGIN = 30;
const mutationSupport = !!window['MutationObserver'];
@ -21,36 +22,39 @@ export default Ember.Component.extend({
const viewMode = this.get('viewMode');
const $panelBody = this.$('.panel-body');
let contentHeight = parseInt(this.$('.panel-body-contents').height());
if (viewMode === 'drop-down') {
const $buttonPanel = $('header ul.icons');
if ($buttonPanel.length === 0) { return; }
const buttonPanelPos = $buttonPanel.offset();
const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height() - $('header.d-header').offset().top);
const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - width);
this.$().css({ left: posLeft + "px", top: posTop + "px" });
// 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.
this.$().css({ top: '100%', height: 'auto' });
// adjust panel height
let contentHeight = parseInt(this.$('.panel-body-contents').height());
const fullHeight = parseInt($window.height());
const offsetTop = this.$().offset().top;
const scrollTop = $window.scrollTop();
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
}
$panelBody.height(contentHeight);
$('body').addClass('drop-down-visible');
} else {
$panelBody.height('auto');
const $header = $('header.d-header');
const headerOffset = $header.offset();
const headerOffsetTop = (headerOffset) ? headerOffset.top : 0;
const headerHeight = parseInt($header.height() + headerOffsetTop - $window.scrollTop() + 3);
this.$().css({ left: "auto", top: headerHeight + "px" });
const menuTop = headerHeight();
let height;
if ((menuTop + contentHeight) < ($(window).height() - 20)) {
height = contentHeight + "px";
} else {
height = $(window).height() - menuTop;
}
$panelBody.height('100%');
this.$().css({ top: menuTop + "px", height });
$('body').removeClass('drop-down-visible');
}
@ -82,7 +86,11 @@ export default Ember.Component.extend({
});
this.performLayout();
this._watchSizeChanges();
// iOS does not handle scroll events well
if (!this.capabilities.touch) {
$(window).on('scroll.discourse-menu-panel', () => this.performLayout());
}
} else {
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden'));
$('html').off('click.close-menu-panel');
@ -124,9 +132,13 @@ export default Ember.Component.extend({
clearInterval(this._resizeInterval);
this._resizeInterval = setInterval(() => {
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(); }
this._lastHeight = contentHeight;
}
});
}, 500);
}
@ -142,7 +154,8 @@ export default Ember.Component.extend({
@on('didInsertElement')
_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; }
this.hide();
});
@ -150,7 +163,7 @@ export default Ember.Component.extend({
this.appEvents.on('dropdowns:closeAll', 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) {
this.hide();
}

View File

@ -1,24 +1,28 @@
/* You might be looking for navigation-item. */
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
router: function() {
@computed()
router() {
return this.container.lookup('router:main');
}.property(),
},
fullPath: function() {
return Discourse.getURL(this.get('path'));
}.property('path'),
@computed("path")
fullPath(path) {
return Discourse.getURL(path);
},
active: function() {
const route = this.get('route');
@computed("route", "router.url")
active(route) {
if (!route) { return; }
const routeParam = this.get('routeParam'),
router = this.get('router');
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';
export default Ember.Component.extend({
tagName: 'ul',
classNameBindings: [':nav', ':nav-pills'],
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];
}.property('filterMode'),
},
closedNav: function(){
@observes("expanded")
closedNav() {
if (!this.get('expanded')) {
this.ensureDropClosed();
}
}.observes('expanded'),
},
ensureDropClosed: function(){
ensureDropClosed() {
if (!this.get('expanded')) {
this.set('expanded',false);
}
@ -30,25 +28,23 @@ export default Ember.Component.extend({
},
actions: {
toggleDrop: function(){
toggleDrop() {
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);
Em.run.next(function() {
Em.run.next(() => {
if (!this.get('expanded')) { return; }
if (!self.get('expanded')) { return; }
self.$('.drop a').on('click', function(){
self.$('.drop').hide();
self.set('expanded', false);
this.$('.drop a').on('click', () => {
this.$('.drop').hide();
this.set('expanded', false);
return true;
});
$(window).on('click.navigation-bar', function() {
self.set('expanded', false);
$(window).on('click.navigation-bar', () => {
this.set('expanded', false);
return true;
});
});

View File

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

View File

@ -4,17 +4,19 @@ export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['notification.read', 'notification.is_warning'],
scope: function() {
name: function() {
var notificationType = this.get("notification.notification_type");
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");
} else {
return "notifications." + name;
return "notifications." + this.get("name");
}
}.property("notification.notification_type"),
}.property("name"),
url: function() {
const it = this.get('notification');
@ -57,7 +59,7 @@ export default Ember.Component.extend({
const url = this.get('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 {
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"),
@observes("shownAt")
bounce(shownAt) {
if (shownAt) {
bounce() {
if (this.get("shownAt")) {
var $elem = this.$();
if (!this.animateAttribute) {
this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left';

View File

@ -3,8 +3,8 @@ export default Ember.Component.extend({
initGaps: function(){
this.set('loading', false);
var before = this.get('before') === 'true',
gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after');
const before = this.get('before') === 'true';
const gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after');
if (gaps) {
this.set('gap', gaps[this.get('post.id')]);
@ -16,29 +16,27 @@ export default Ember.Component.extend({
this.rerender();
}.observes('post.hasGap'),
render: function(buffer) {
render(buffer) {
if (this.get('loading')) {
buffer.push(I18n.t('loading'));
} else {
var gapLength = this.get('gap.length');
const gapLength = this.get('gap.length');
if (gapLength) {
buffer.push(I18n.t('post.gap', {count: gapLength}));
}
}
},
click: function() {
click() {
if (this.get('loading') || (!this.get('gap'))) { return false; }
this.set('loading', true);
this.rerender();
var self = this,
postStream = this.get('postStream'),
filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter;
const postStream = this.get('postStream');
const filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter;
filler.call(postStream, this.get('post'), this.get('gap')).then(function() {
// hide this control after the promise is resolved
self.set('gap', null);
filler.call(postStream, this.get('post'), this.get('gap')).then(() => {
this.set('gap', null);
});
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 { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import showModal from 'discourse/lib/show-modal';
@ -48,18 +48,7 @@ export default Ember.Component.extend({
@computed('searchService.searchContext')
searchContextDescription(ctx) {
if (ctx) {
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');
}
}
return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name'));
},
@observes('searchService.searchContextEnabled')
@ -72,8 +61,8 @@ export default Ember.Component.extend({
@observes('searchService.term', 'typeFilter')
newSearchNeeded() {
this.set('noResults', false);
const term = (this.get('searchService.term') || '').trim();
if (term.length >= Discourse.SiteSettings.min_search_term_length) {
const term = this.get('searchService.term')
if (isValidSearchTerm(term)) {
this.set('loading', true);
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
} else {
@ -145,7 +134,7 @@ export default Ember.Component.extend({
},
showedSearch() {
$('#search-term').focus();
$('#search-term').focus().select();
},
showSearchHelp() {
@ -165,8 +154,7 @@ export default Ember.Component.extend({
},
keyDown(e) {
const term = this.get('searchService.term');
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
this.set('visible', false);
this.send('fullSearch');
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { url } from 'discourse/lib/computed';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import { headerHeight } from 'discourse/views/header';
export default Ember.Component.extend({
classNames: ['user-menu'],
@ -17,8 +18,8 @@ export default Ember.Component.extend({
showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; },
@observes('visible')
_loadNotifications(visible) {
if (visible) {
_loadNotifications() {
if (this.get("visible")) {
this.refreshNotifications();
}
},
@ -43,13 +44,30 @@ export default Ember.Component.extend({
refreshNotifications() {
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
// wants to reach out and grab notifications
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) {
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 {
this.set('loadingNotifications', true);
}

View File

@ -25,7 +25,7 @@ export default TextField.extend({
dataSource: function(term) {
return userSearch({
term: term.replace(/[^a-zA-Z0-9_]/, ''),
term: term.replace(/[^a-zA-Z0-9_\-\.]/, ''),
topicId: self.get('topicId'),
exclude: excludedUsernames(),
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 += Discourse.Utilities.avatarImg({
size: 'small',
avatarTemplate: u.get('avatarTemplate'),
avatarTemplate: u.get('avatar_template'),
title: u.get('username')
});
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, {
uploadedAvatarTemplate: null,
saveDisabled: Em.computed.alias("uploading"),
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'),
selectedUploadId: function() {
switch (this.get("selected")) {
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");
@computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
selectedUploadId(selected, system, gravatar, custom) {
switch (selected) {
case "system": return system;
case "gravatar": return gravatar;
default: return custom;
}
}.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();
}.property(),
},
actions: {
useUploadedAvatar() { this.set("selected", "uploaded"); },
@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
refreshGravatar() {
this.set("gravatarRefreshDisabled", true);
return Discourse
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' })
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id))
.ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
.then(result => this.setProperties({
gravatar_avatar_template: result.gravatar_avatar_template,
gravatar_upload_id: result.gravatar_upload_id,
}))
.finally(() => this.set("gravatarRefreshDisabled", false));
}
}

View File

@ -39,11 +39,11 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, {
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
self.send('closeModal');
self.get('topicController').send('toggleMultiSelect');
Em.run.next(function() { DiscourseURL.routeTo(result.url); });
Em.run.next(() => { DiscourseURL.routeTo(self.get("topicController.model.url")); });
}, function() {
// failure
self.flash(I18n.t('topic.change_owner.error'), 'alert-error');

View File

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

View File

@ -2,6 +2,45 @@ import { setting } from 'discourse/lib/computed';
import DiscourseURL from 'discourse/lib/url';
import Quote from 'discourse/lib/quote';
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({
needs: ['modal', 'topic', 'composer-messages', 'application'],
@ -26,6 +65,12 @@ export default Ember.Controller.extend({
this.set('similarTopics', []);
}.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() {
if (!Discourse.User.currentProp('staff')) { return false; }
@ -94,7 +139,6 @@ export default Ember.Controller.extend({
},
hitEsc() {
const messages = this.get('controllers.composer-messages.model');
if (messages.length) {
messages.popObject();
@ -438,7 +482,7 @@ export default Ember.Controller.extend({
// Given a potential instance and options, set the model for this composer.
_setModel(composerModel, opts) {
if (opts.draft) {
composerModel = Discourse.Composer.loadDraft(opts);
composerModel = loadDraft(this.store, opts);
if (composerModel) {
composerModel.set('topic', opts.topic);
}

View File

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

View File

@ -29,8 +29,8 @@ const controllerOpts = {
},
// Show newly inserted topics
showInserted: function() {
const tracker = Discourse.TopicTrackingState.current();
showInserted() {
const tracker = this.topicTrackingState;
// Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming'));
@ -38,9 +38,8 @@ const controllerOpts = {
return false;
},
refresh: function() {
const filter = this.get('model.filter'),
self = this;
refresh() {
const filter = this.get('model.filter');
this.setProperties({ order: 'default', ascending: false });
@ -52,36 +51,27 @@ const controllerOpts = {
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
this.store.findFiltered('topicList', {filter}).then(function(list) {
Discourse.TopicList.hideUniformCategory(list, self.get('category'));
this.store.findFiltered('topicList', {filter}).then((list) => {
Discourse.TopicList.hideUniformCategory(list, this.get('category'));
self.setProperties({ model: list });
self.resetSelected();
this.setProperties({ model: list });
this.resetSelected();
const tracking = Discourse.TopicTrackingState.current();
if (tracking) {
tracking.sync(list, filter);
if (this.topicTrackingState) {
this.topicTrackingState.sync(list, filter);
}
self.send('loadingComplete');
this.send('loadingComplete');
});
},
resetNew: function() {
const self = this;
Discourse.TopicTrackingState.current().resetNew();
Discourse.Topic.resetNew().then(function() {
self.send('refresh');
});
resetNew() {
this.topicTrackingState.resetNew();
Discourse.Topic.resetNew().then(() => this.send('refresh'));
}
},
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),
isFilterPage: function(filter, filterType) {
if (!filter) { return 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;
}.property('model.filter', 'model.topics.length'),
tooManyTracked: function(){
return Discourse.TopicTrackingState.current().tooManyTracked();
}.property(),
showDismissAtTop: function() {
return (this.isFilterPage(this.get('model.filter'), 'new') ||
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';
// Modal related to auto closing of topics
@ -5,31 +6,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
auto_close_valid: true,
auto_close_invalid: Em.computed.not('auto_close_valid'),
setAutoCloseTime: function() {
var autoCloseTime = null;
@observes("model.details.auto_close_at", "model.details.auto_close_hours")
setAutoCloseTime() {
let autoCloseTime = null;
if (this.get("model.details.auto_close_based_on_last_post")) {
autoCloseTime = this.get("model.details.auto_close_hours");
} 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()) {
autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm");
}
}
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) {
var self = this;
actions: {
saveAutoClose() { this.setAutoClose(this.get("model.auto_close_time")); },
removeAutoClose() { this.setAutoClose(null); }
},
setAutoClose(time) {
const self = this;
this.send('hideModal');
Discourse.ajax({
url: '/t/' + this.get('model.id') + '/autoclose',
url: `/t/${this.get('model.id')}/autoclose`,
type: 'PUT',
dataType: 'json',
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"),
timezone_offset: (new Date().getTimezoneOffset())
}
}).then(function(result){
}).then(result => {
if (result.success) {
self.send('closeModal');
self.set('model.details.auto_close_at', result.auto_close_at);
self.set('model.details.auto_close_hours', result.auto_close_hours);
this.send('closeModal');
this.set('model.details.auto_close_at', result.auto_close_at);
this.set('model.details.auto_close_hours', result.auto_close_hours);
} else {
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'); } );
});
}

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({
needs: ["application"],
loading: Em.computed.not("model"),
queryParams: ["q"],
queryParams: ["q", "context_id", "context", "skip_context"],
q: null,
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")) {
this.set("searchTerm", this.get("q"));
}
}.observes("model"),
},
qChanged: function() {
@observes('q')
qChanged() {
const model = this.get("model");
if (model && this.get("model.q") !== this.get("q")) {
this.set("searchTerm", this.get("q"));
this.send("search");
}
}.observes("q"),
},
_showFooter: function() {
@observes('loading')
_showFooter() {
this.set("controllers.application.showFooter", !this.get("loading"));
}.observes("loading"),
},
canBulkSelect: Em.computed.alias('currentUser.staff'),
search(){
if (this._searching) {
return;
}
this._searching = true;
const router = Discourse.__container__.lookup('router:main');
this.set("q", this.get("searchTerm"));
this.set("model", null);
Discourse.ajax("/search", { data: { q: this.get("searchTerm") } }).then(results => {
this.set("model", translateResults(results) || {});
this.set("model.q", this.get("q"));
});
var args = { q: this.get("searchTerm") };
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: {
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() {
this.toggleProperty('bulkSelectEnabled');
this.get('selected').clear();
@ -51,7 +131,15 @@ export default Ember.Controller.extend({
this.search();
},
showSearchHelp() {
// TODO: dupe code should be centralized
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
showModal('searchHelp', { model });
});
},
search() {
if (this.get("isNotValidSearchTerm")) return;
this.search();
}
}

View File

@ -1,3 +1,5 @@
import DiscourseURL from 'discourse/lib/url';
const HeaderController = Ember.Controller.extend({
topic: null,
showExtraInfo: null,
@ -18,6 +20,24 @@ const HeaderController = Ember.Controller.extend({
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) {
this.toggleProperty(visibleProp);
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 { setting } from 'discourse/lib/computed';
@ -6,8 +7,9 @@ export default NavigationDefaultController.extend({
showingParentCategory: Em.computed.none('category.parentCategory'),
showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'),
navItems: function() {
if (this.get('showingSubcategoryList')) { return []; }
return Discourse.NavItem.buildList(this.get('category'), { noSubcategories: this.get('noSubcategories') });
}.property('category', 'noSubcategories')
@computed("showingSubcategoryList", "category", "noSubcategories")
navItems(showingSubcategoryList, category, noSubcategories) {
if (showingSubcategoryList) { return []; }
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({
needs: ['discovery', 'discovery/topics'],
categories: function() {
@computed()
categories() {
return Discourse.Category.list();
}.property(),
},
navItems: function() {
return Discourse.NavItem.buildList(null, {filterMode: this.get('filterMode')});
}.property('filterMode')
@computed("filterMode")
navItems(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 CanCheckEmails from 'discourse/mixins/can-check-emails';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(CanCheckEmails, {
@ -10,18 +11,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
allowBackgrounds: setting('allow_profile_backgrounds'),
editHistoryVisible: setting('edit_history_visible_to_public'),
selectedCategories: function(){
return [].concat(this.get("model.watchedCategories"),
this.get("model.trackedCategories"),
this.get("model.mutedCategories"));
}.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"),
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
selectedCategories(watched, tracked, muted) {
return [].concat(watched, tracked, muted);
},
// By default we haven't saved anything
saved: false,
newNameInput: null,
userFields: function() {
@computed("model.user_fields.@each.value")
userFields() {
let siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('model.user_fields');
@ -35,34 +36,37 @@ export default Ember.Controller.extend(CanCheckEmails, {
return Ember.Object.create({ value, field });
});
}
}.property('model.user_fields.@each.value'),
},
cannotDeleteAccount: Em.computed.not('can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
canEditName: setting('enable_names'),
nameInstructions: function() {
@computed()
nameInstructions() {
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
}.property(),
},
canSelectTitle: function() {
return this.siteSettings.enable_badges && this.get('model.has_title_badges');
}.property('model.badge_count'),
@computed("model.has_title_badges")
canSelectTitle(hasTitleBadges) {
return this.siteSettings.enable_badges && hasTitleBadges;
},
canChangePassword: function() {
@computed()
canChangePassword() {
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
}.property(),
},
canReceiveDigest: function() {
@computed()
canReceiveDigest() {
return !this.siteSettings.disable_digest_emails;
}.property(),
},
availableLocales: function() {
return this.siteSettings.available_locales.split('|').map( function(s) {
return {name: s, value: s};
});
}.property(),
@computed()
availableLocales() {
return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s }));
},
digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 },
{ 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.last_here'), value: -2 }],
saveButtonText: function() {
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save');
}.property('model.isSaving'),
@computed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t('saving') : I18n.t('save');
},
passwordProgress: null,
actions: {
save() {
const self = this;
this.set('saved', false);
const model = this.get('model');
@ -113,28 +117,27 @@ export default Ember.Controller.extend(CanCheckEmails, {
// Cook the bio for preview
model.set('name', this.get('newNameInput'));
return model.save().then(function() {
return model.save().then(() => {
if (Discourse.User.currentProp('id') === model.get('id')) {
Discourse.User.currentProp('name', model.get('name'));
}
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
self.set('saved', true);
this.set('saved', true);
}).catch(popupAjaxError);
},
changePassword() {
const self = this;
if (!this.get('passwordProgress')) {
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
self.setProperties({
this.setProperties({
changePasswordProgress: false,
passwordProgress: I18n.t("user.change_password.success")
});
}, function() {
}).catch(() => {
// password failed to change
self.setProperties({
this.setProperties({
changePasswordProgress: false,
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: {
markFaqRead() {
if (this.currentUser) {
Discourse.ajax("/users/read-faq", { method: "POST" });
const currentUser = this.currentUser;
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';
function entranceDate(dt, showTime) {
var today = new Date();
const today = new Date();
if (dt.toDateString() === today.toDateString()) {
return moment(dt).format(I18n.t("dates.time"));
@ -44,7 +44,7 @@ export default Ember.Controller.extend({
}.property('bumpedDate'),
actions: {
show: function(data) {
show(data) {
// Show the chooser but only if the model changes
if (this.get('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'));
},
enterBottom: function() {
enterBottom() {
DiscourseURL.routeTo(this.get('model.lastPostUrl'));
}
}

View File

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

View File

@ -37,7 +37,7 @@ export default Ember.Controller.extend({
show(username, postId, target) {
// 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
if (Discourse.Mobile.mobileView) {

View File

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

View File

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

View File

@ -501,6 +501,12 @@ Discourse.Dialect = {
var pos = args.start.lastIndex - match[0].length,
leading = block.slice(0, pos),
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
args.stop.lastIndex = block.length - trailing.length;
if (!args.stop.exec(block) && lastChance()) { return; }

View File

@ -7,7 +7,7 @@ Discourse.Dialect.inlineRegexp({
start: '@',
// 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
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,
emitter: function(matches) {

View File

@ -1,37 +1,29 @@
import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter';
const safe = Handlebars.SafeString;
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
Em.Handlebars.helper('bound-avatar', (user, size) => {
if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>");
}
const username = Em.get(user, 'username');
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId);
const avatar = Em.get(user, 'avatar_template');
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
*/
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 }));
});
registerUnbound('raw-date', function(dt) {
return longDate(new Date(dt));
});
registerUnbound('raw-date', dt => longDate(new Date(dt)));
registerUnbound('age-with-tooltip', function(dt) {
return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}));
});
registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
registerUnbound('number', function(orig, params) {
registerUnbound('number', (orig, params) => {
orig = parseInt(orig, 10);
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;
function findOutlets(collection, callback) {
@ -63,9 +70,17 @@ function findOutlets(collection, callback) {
}
const segments = res.split("/");
const outletName = segments[segments.length-2];
let outletName = segments[segments.length-2];
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, '-');
if (dashedName !== outletName) {
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 avatarTemplate from 'discourse/lib/avatar-template';
function renderAvatar(user, options) {
options = options || {};
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) {
// first try to get a title
title = Em.get(user, 'title');
// if there was no title provided
if (!title) {
// try to retrieve a description
var description = Em.get(user, 'description');
const description = Em.get(user, 'description');
// if a description has been provided
if (description && description.length > 0) {
// 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({
size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses,
title: title || username,
avatarTemplate: avatarTemplate(username, uploadedAvatarId)
avatarTemplate: avatarTemplate
});
} else {
return '';

View File

@ -1,37 +1,10 @@
import interceptClick from 'discourse/lib/intercept-click';
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 {
name: "click-interceptor",
initialize: function() {
$('#main').on('click.discourse', 'a', function(e) {
if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; }
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;
});
initialize() {
$('#main').on('click.discourse', 'a', interceptClick);
$(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash));
}
};

View File

@ -6,6 +6,9 @@ export default {
initialize(container) {
const cache = {};
var transitionCount = 0;
// Tell our AJAX system to track a page transition
const router = container.lookup('router:main');
router.on('willTransition', function() {
@ -14,7 +17,22 @@ export default {
router.on('didTransition', function() {
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();
pageTracker.start();

View File

@ -8,7 +8,12 @@ export default {
const user = container.lookup('current-user:main'),
site = container.lookup('site: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) {
@ -38,6 +43,32 @@ export default {
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
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);
bus.subscribe("/categories", function(data) {

View File

@ -91,7 +91,7 @@ export default function(options) {
transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item];
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');
if (prev.length === 0) {
me.parent().prepend(d);
@ -220,6 +220,13 @@ export default function(options) {
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 borderTop = parseInt(me.css('border-top-width'), 10) || 0;
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();
var url = urlFor(code);
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) {
if (!safeLocalStorage) { return; }
return safeLocalStorage.removeItem(this.context + key);
},
@ -51,6 +52,13 @@ KeyValueStore.prototype = {
const result = parseInt(this.get(key));
if (!isFinite(result)) { return def; }
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';
const PATH_BINDINGS = {
'g h': '/',
'g l': '/latest',
'g n': '/new',
'g u': '/unread',
'g c': '/categories',
'g t': '/top',
'g b': '/bookmarks',
'g p': '/my/activity'
},
SELECTED_POST_BINDINGS = {
'd': 'deletePost',
'e': 'editPost',
'l': 'toggleLike',
'r': 'replyToPost',
'!': 'showFlags',
't': 'replyAsNewTopic'
},
CLICK_BINDINGS = {
'm m': 'div.notification-options li[data-id="0"] a', // mark topic as muted
'm r': 'div.notification-options li[data-id="1"] a', // mark topic as regular
'm t': 'div.notification-options li[data-id="2"] a', // mark topic as tracking
'm w': 'div.notification-options li[data-id="3"] a', // mark topic as watching
'x r': '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top', // dismiss new/posts
'x t': '#dismiss-topics,#dismiss-topics-top', // dismiss topics
'.': '.alert.alert-info.clickable', // show incoming/updated topics
'o,enter': '.topic-list tr.selected a.title', // open selected topic
'shift+s': '#topic-footer-buttons button.share', // share topic
's': '.topic-post.selected a.post-date' // share post
},
FUNCTION_BINDINGS = {
'c': 'createTopic', // create new topic
'home': 'goToFirstPost',
'#': 'toggleProgress',
'end': 'goToLastPost',
'shift+j': 'nextSection',
'j': 'selectDown',
'shift+k': 'prevSection',
'shift+p': 'pinUnpinTopic',
'k': 'selectUp',
'u': 'goBack',
'/': 'showSearch',
'=': 'toggleHamburgerMenu',
'p': 'showCurrentUser', // open current user menu
'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'
};
const bindings = {
'!': {postAction: 'showFlags'},
'#': {handler: 'toggleProgress', anonymous: true},
'/': {handler: 'showSearch', anonymous: true},
'=': {handler: 'toggleHamburgerMenu', anonymous: true},
'?': {handler: 'showHelpModal', anonymous: true},
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
'b': {handler: 'toggleBookmark'},
'c': {handler: 'createTopic'},
'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true},
'command+f': {handler: 'showBuiltinSearch', anonymous: true},
'd': {postAction: 'deletePost'},
'e': {postAction: 'editPost'},
'end': {handler: 'goToLastPost', anonymous: true},
'f': {handler: 'toggleBookmarkTopic'},
'g h': {path: '/', anonymous: true},
'g l': {path: '/latest', anonymous: true},
'g n': {path: '/new'},
'g u': {path: '/unread'},
'g c': {path: '/categories', anonymous: true},
'g t': {path: '/top', anonymous: true},
'g b': {path: '/bookmarks'},
'g p': {path: '/my/activity'},
'g m': {path: '/my/messages'},
'home': {handler: 'goToFirstPost', anonymous: true},
'j': {handler: 'selectDown', anonymous: true},
'k': {handler: 'selectUp', anonymous: true},
'l': {postAction: 'toggleLike'},
'm m': {click: 'div.notification-options li[data-id="0"] a'}, // mark topic as muted
'm r': {click: 'div.notification-options li[data-id="1"] a'}, // mark topic as regular
'm t': {click: 'div.notification-options li[data-id="2"] a'}, // mark topic as tracking
'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
'p': {handler: 'showCurrentUser'},
'q': {handler: 'quoteReply'},
'r': {postAction: 'replyToPost'},
's': {click: '.topic-post.selected a.post-date', anonymous: true}, // share post
'shift+j': {handler: 'nextSection', anonymous: true},
'shift+k': {handler: 'prevSection', anonymous: true},
'shift+p': {handler: 'pinUnpinTopic'},
'shift+r': {handler: 'replyToTopic'},
'shift+s': {click: '#topic-footer-buttons button.share', anonymous: true}, // share topic
'shift+z shift+z': {handler: 'logout'},
't': {postAction: 'replyAsNewTopic'},
'u': {handler: 'goBack', anonymous: true},
'x r': {click: '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top'}, // dismiss new/posts
'x t': {click: '#dismiss-topics,#dismiss-topics-top'} // dismiss topics
};
export default {
@ -64,14 +56,24 @@ export default {
this.container = container;
this._stopCallback();
this.searchService = this.container.lookup('search-service:main');
this.appEvents = this.container.lookup('app-events:main');
this.currentUser = this.container.lookup('current-user:main');
_.each(PATH_BINDINGS, this._bindToPath, this);
_.each(CLICK_BINDINGS, this._bindToClick, this);
_.each(SELECTED_POST_BINDINGS, this._bindToSelectedPost, this);
_.each(FUNCTION_BINDINGS, this._bindToFunction, this);
Object.keys(bindings).forEach(key => {
const binding = bindings[key];
if (!binding.anonymous && !this.currentUser) { return; }
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() {
@ -222,17 +224,11 @@ export default {
},
_bindToSelectedPost(action, binding) {
const self = this;
this.keyTrapper.bind(binding, function() {
self.sendToSelectedPost(action);
});
this.keyTrapper.bind(binding, () => this.sendToSelectedPost(action));
},
_bindToPath(path, binding) {
this.keyTrapper.bind(binding, function() {
DiscourseURL.routeTo(path);
});
_bindToPath(path, key) {
this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path));
},
_bindToClick(selector, binding) {

View File

@ -1,11 +1,8 @@
function applicable() {
// CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching)
// Dolphin has a wierd user agent, rest seem a bit nitch
// This will apply hack on all iDevices
return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
navigator.userAgent.match(/Safari/g) &&
!navigator.userAgent.match(/CriOS/g) &&
!navigator.userAgent.match(/OPiOS/g);
navigator.userAgent.match(/Safari/g);
}
// 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];
var done = false;
var originalScrollTop = 0;
var blurredNow = function(evt) {
if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
@ -25,8 +23,16 @@ function positioningWorkaround($fixedElement) {
}
done = true;
fixedElement.parentElement.style.height = '';
$('#main-outlet').show();
$('header').show();
fixedElement.style.position = '';
fixedElement.style.top = '';
fixedElement.style.height = '';
$(window).scrollTop(originalScrollTop);
if (evt) {
evt.target.removeEventListener('blur', blurred);
}
@ -50,31 +56,23 @@ function positioningWorkaround($fixedElement) {
return;
}
originalScrollTop = $(window).scrollTop();
// take care of body
$('#main-outlet').hide();
$('header').hide();
fixedElement.style.position = 'absolute';
// get out of the way while opening keyboard
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();
self.focus();

View File

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

View File

@ -86,4 +86,32 @@ function searchForTerm(term, opts) {
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
keep the history intact.
**/
routeTo: function(path, opts) {
routeTo(path, opts) {
if (Em.isEmpty(path)) { return; }
if (Discourse.get('requiresRefresh')) {
@ -122,6 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
// Scroll to the same page, different anchor
if (path.indexOf('#') === 0) {
this.scrollToId(path);
history.replaceState(undefined, undefined, path);
return;
}
@ -271,7 +272,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
// This has been extracted so it can be tested.
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) {
// TODO site setting for allowed regex in username
if (term.match(/[^a-zA-Z0-9_\.]/)) {
if (term.match(/[^a-zA-Z0-9_\.\-]/)) {
resolve([]);
return;
}

View File

@ -97,7 +97,10 @@ Discourse.Utilities = {
// Strip out any .click elements from the HTML before converting it to text
var div = document.createElement('div');
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 || "";
return String(text).trim();
@ -212,6 +215,10 @@ Discourse.Utilities = {
}
},
getUploadPlaceholder: function(filename) {
return "[" + I18n.t("uploading_filename", { filename: filename }) + "]() ";
},
isAnImage: function(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) {
if (result && result.topic_ids) {
const tracker = Discourse.TopicTrackingState.current();
const tracker = self.topicTrackingState;
result.topic_ids.forEach(function(t) {
tracker.removeTopic(t);
});

View File

@ -1,5 +1,6 @@
import Eyeline from 'discourse/lib/eyeline';
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.
export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
@ -9,15 +10,23 @@ export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
if (eyeline) { eyeline.update(); }
},
_bindEyeline: function() {
loadMoreUnlessFull() {
if (this.screenNotFull()) {
this.send("loadMore");
}
},
@on("didInsertElement")
_bindEyeline() {
const eyeline = new Eyeline(this.get('eyelineSelector') + ":last");
this.set('eyeline', eyeline);
eyeline.on('sawBottom', () => this.send('loadMore'));
this.bindScrolling();
}.on('didInsertElement'),
},
_removeEyeline: function() {
@on("willDestroyElement")
_removeEyeline() {
this.unbindScrolling();
}.on('willDestroyElement')
}
});

View File

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

View File

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

View File

@ -8,10 +8,11 @@ export default {
return `${type}_${hashedArgs}`;
},
findStale(store, type, findArgs) {
findStale(store, type, findArgs, opts) {
const staleResult = new StaleResult();
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
try {
const stored = this.keyValueStore.getItem(this.storageKey(type, findArgs));
const stored = this.keyValueStore.getItem(key);
if (stored) {
const parsed = JSON.parse(stored);
staleResult.setResults(parsed);
@ -22,9 +23,11 @@ export default {
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) => {
this.keyValueStore.setItem(this.storageKey(type, findArgs), JSON.stringify(results));
this.keyValueStore.setItem(key, JSON.stringify(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() {
this._super();
var availableGroups = Em.A(this.get("available_groups"));
const Category = RestModel.extend({
@on('init')
setupGroupsAndPermissions() {
const availableGroups = this.get('available_groups');
if (!availableGroups) { return; }
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);
return {
group_name: elem.group_name,
permission: Discourse.PermissionType.create({id: elem.permission_type})
};
})));
}));
}
},
availablePermissions: function(){
@ -26,7 +33,7 @@ Discourse.Category = Discourse.Model.extend({
}.property('id'),
url: function() {
return Discourse.getURL("/c/") + Discourse.Category.slugFor(this);
return Discourse.getURL("/c/") + Category.slugFor(this);
}.property('name'),
fullSlug: function() {
@ -77,7 +84,8 @@ Discourse.Category = Discourse.Model.extend({
background_url: this.get('background_url'),
allow_badges: this.get('allow_badges'),
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'
});
@ -128,16 +136,12 @@ Discourse.Category = Discourse.Model.extend({
}
}.property('topics'),
topicTrackingState: function(){
return Discourse.TopicTrackingState.current();
}.property(),
unreadTopics: function(){
return this.get('topicTrackingState').countUnread(this.get('id'));
unreadTopics: function() {
return this.topicTrackingState.countUnread(this.get('id'));
}.property('topicTrackingState.messageCount'),
newTopics: function(){
return this.get('topicTrackingState').countNew(this.get('id'));
newTopics: function() {
return this.topicTrackingState.countNew(this.get('id'));
}.property('topicTrackingState.messageCount'),
topicStatsTitle: function() {
@ -192,83 +196,78 @@ Discourse.Category = Discourse.Model.extend({
var _uncategorized;
Discourse.Category.reopenClass({
Category.reopenClass({
findUncategorized: function() {
_uncategorized = _uncategorized || Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
findUncategorized() {
_uncategorized = _uncategorized || Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
return _uncategorized;
},
slugFor: function(category) {
slugFor(category) {
if (!category) return "";
var parentCategory = Em.get(category, 'parentCategory'),
result = "";
const parentCategory = Em.get(category, 'parentCategory');
let result = "";
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');
if (!slug || slug.trim().length === 0) return result + id + "-category";
return result + slug;
return !slug || slug.trim().length === 0 ? `${result}${id}-category` : result + slug;
},
list: function() {
if (Discourse.SiteSettings.fixed_category_positions) {
return Discourse.Site.currentProp('categories');
} else {
return Discourse.Site.currentProp('sortedCategories');
}
list() {
return Discourse.SiteSettings.fixed_category_positions ?
Discourse.Site.currentProp('categories') :
Discourse.Site.currentProp('sortedCategories');
},
listByActivity: function() {
listByActivity() {
return Discourse.Site.currentProp('sortedCategories');
},
idMap: function() {
idMap() {
return Discourse.Site.currentProp('categoriesById');
},
findSingleBySlug: function(slug) {
return Discourse.Category.list().find(function(c) {
return Discourse.Category.slugFor(c) === slug;
});
findSingleBySlug(slug) {
return Category.list().find(c => Category.slugFor(c) === slug);
},
findById: function(id) {
findById(id) {
if (!id) { return; }
return Discourse.Category.idMap()[id];
return Category.idMap()[id];
},
findByIds: function(ids){
var categories = [];
_.each(ids, function(id){
var found = Discourse.Category.findById(id);
if(found){
findByIds(ids) {
const categories = [];
_.each(ids, id => {
const found = Category.findById(id);
if (found) {
categories.push(found);
}
});
return categories;
},
findBySlug: function(slug, parentSlug) {
var categories = Discourse.Category.list(),
category;
findBySlug(slug, parentSlug) {
const categories = Category.list();
let category;
if (parentSlug) {
var parentCategory = Discourse.Category.findSingleBySlug(parentSlug);
const parentCategory = Category.findSingleBySlug(parentSlug);
if (parentCategory) {
if (slug === 'none') { return parentCategory; }
category = categories.find(function(item) {
return item && item.get('parentCategory') === parentCategory && Discourse.Category.slugFor(item) === (parentSlug + "/" + slug);
category = categories.find(item => {
return item && item.get('parentCategory') === parentCategory && Category.slugFor(item) === (parentSlug + "/" + slug);
});
}
} else {
category = Discourse.Category.findSingleBySlug(slug);
category = Category.findSingleBySlug(slug);
// If we have a parent category, we need to enforce it
if (category && category.get('parentCategory')) return;
@ -282,9 +281,9 @@ Discourse.Category.reopenClass({
return category;
},
reloadById: function(id) {
return Discourse.ajax("/c/" + id + "/show.json").then(function (result) {
return Discourse.Category.create(result.category);
});
reloadById(id) {
return Discourse.ajax(`/c/${id}/show.json`);
}
});
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 Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft';
import computed from 'ember-addons/ember-computed-decorators';
const CLOSED = 'closed',
SAVING = 'saving',
@ -23,6 +24,7 @@ const CLOSED = 'closed',
category: 'categoryId',
topic_id: 'topic.id',
is_warning: 'isWarning',
whisper: 'whisper',
archetype: 'archetypeId',
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
@ -35,15 +37,42 @@ const CLOSED = 'closed',
};
const Composer = RestModel.extend({
_categoryId: null,
archetypes: function() {
return this.site.get('archetypes');
}.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),
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
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(){
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
}.property('creatingPrivateMessage', 'topic'),
@ -56,6 +85,7 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal('composeState', OPEN),
viewDraft: Em.computed.equal('composeState', DRAFT),
composeStateChanged: function() {
var oldOpen = this.get('composerOpened');
@ -339,20 +369,24 @@ const Composer = RestModel.extend({
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
},
applyTopicTemplate: function() {
applyTopicTemplate(oldCategoryId, categoryId) {
if (this.get('action') !== CREATE_TOPIC) { return; }
if (!Ember.isEmpty(this.get('reply'))) { return; }
let reply = this.get('reply');
const categoryId = this.get('categoryId');
const category = this.site.categories.find((c) => c.get('id') === categoryId);
// If the user didn't change the template, clear it
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) {
const topicTemplate = category.get('topic_template');
if (!Ember.isEmpty(topicTemplate)) {
this.set('reply', topicTemplate);
this.set('reply', category.get('topic_template') || "");
}
}
}.observes('categoryId'),
},
/*
Open a composer
@ -397,14 +431,22 @@ const Composer = RestModel.extend({
}
}
const categoryId = opts.categoryId || this.get('topic.category.id');
this.setProperties({
categoryId,
archetypeId: opts.archetypeId || this.site.get('default_archetype'),
metaData: opts.metaData ? Em.Object.create(opts.metaData) : null,
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) {
this.set('loading', true);
this.store.find('post', opts.postId).then(function(post) {
@ -529,6 +571,9 @@ const Composer = RestModel.extend({
let addedToStream = false;
const postTypes = this.site.get('post_types');
const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular;
// Build the post object
const createdPost = this.store.createRecord('post', {
imageSizes: opts.imageSizes,
@ -539,9 +584,9 @@ const Composer = RestModel.extend({
username: user.get('username'),
user_id: user.get('id'),
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'),
post_type: this.site.get('post_types.regular'),
post_type: postType,
actions_summary: [],
moderator: user.get('moderator'),
admin: user.get('admin'),
@ -559,7 +604,7 @@ const Composer = RestModel.extend({
reply_to_post_number: post.get('post_number'),
reply_to_user: {
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({
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
create(args) {
args = args || {};

View File

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

View File

@ -5,10 +5,7 @@ function calcDayDiff(p1, p2) {
if (!p1) { return; }
const date = p1.get('created_at');
if (date) {
if (p2) {
const numDiff = p1.get('post_number') - p2.get('post_number');
if (numDiff === 1) {
if (date && p2) {
const lastDate = p2.get('created_at');
if (lastDate) {
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
@ -17,8 +14,6 @@ function calcDayDiff(p1, p2) {
p1.set('daysSincePrevious', days);
}
}
}
}
}
const PostStream = RestModel.extend({
@ -282,13 +277,12 @@ const PostStream = RestModel.extend({
fillGapAfter(post, gap) {
const postId = post.get('id'),
stream = this.get('stream'),
idx = stream.indexOf(postId),
self = this;
idx = stream.indexOf(postId);
if (idx !== -1) {
stream.pushObjects(gap);
return this.appendMore().then(function() {
self.get('stream').enumerableContentDidChange();
return this.appendMore().then(() => {
this.get('stream').enumerableContentDidChange();
});
}
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.
appendMore() {
const self = this;
// 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(); }
self.set('loadingBelow', true);
this.set('loadingBelow', true);
const stopLoading = function() {
self.set('loadingBelow', false);
};
const stopLoading = () => this.set('loadingBelow', false);
return self.findPostsByIds(postIds).then(function(posts) {
posts.forEach(function(p) {
self.appendPost(p);
});
return this.findPostsByIds(postIds).then((posts) => {
posts.forEach(p => this.appendPost(p));
stopLoading();
}, stopLoading);
},
@ -685,6 +673,12 @@ const PostStream = RestModel.extend({
const postIdentityMap = this.get('postIdentityMap'),
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 the post is in the identity map, update it and return the old reference.
existing.updateFromPost(post);
@ -693,12 +687,6 @@ const PostStream = RestModel.extend({
post.set('topic', this.get('topic'));
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;
},

View File

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

View File

@ -78,13 +78,10 @@ RestModel.reopenClass({
create(args) {
args = args || {};
if (!args.store || !args.keyValueStore) {
if (!args.store) {
const container = Discourse.__container__;
// Ember.warn('Use `store.createRecord` to create records instead of `.create()`');
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;

View File

@ -1,35 +1,37 @@
import computed from "ember-addons/ember-computed-decorators";
import Archetype from 'discourse/models/archetype';
import PostActionType from 'discourse/models/post-action-type';
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'),
notificationLookup: function() {
@computed("notification_types")
notificationLookup(notificationTypes) {
const result = [];
_.each(this.get('notification_types'), function(v,k) {
result[v] = k;
});
_.each(notificationTypes, (v, k) => result[v] = k);
return result;
}.property('notification_types'),
},
flagTypes: function() {
@computed("post_action_types.@each")
flagTypes() {
const postActionTypes = this.get('post_action_types');
if (!postActionTypes) return [];
return postActionTypes.filterProperty('is_flag', true);
}.property('post_action_types.@each'),
},
topicCountDesc: ['topic_count:desc'],
categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'),
// Sort subcategories under parents
sortedCategories: function() {
const cats = this.get('categoriesByCount'),
result = [],
@computed("categoriesByCount", "categories.@each")
sortedCategories(cats) {
const result = [],
remaining = {};
cats.forEach(function(c) {
cats.forEach(c => {
const parentCategoryId = parseInt(c.get('parent_category_id'), 10);
if (!parentCategoryId) {
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)),
index = result.indexOf(category);
if (index !== -1) {
result.replace(index+1, 0, remaining[parentCategoryId]);
result.replace(index + 1, 0, remaining[parentCategoryId]);
}
});
return result;
}.property("categories.@each"),
},
postActionTypeById(id) {
return this.get("postActionByIdLookup.action" + id);
@ -80,7 +82,7 @@ const Site = Discourse.Model.extend({
existingCategory.setProperties(newCategory);
} else {
// TODO insert in right order?
newCategory = Discourse.Category.create(newCategory);
newCategory = this.store.createRecord('category', newCategory);
categories.pushObject(newCategory);
this.get('categoriesById')[categoryId] = newCategory;
}
@ -91,20 +93,20 @@ Site.reopenClass(Singleton, {
// The current singleton will retrieve its attributes from the `PreloadStore`.
createCurrent() {
return Site.create(PreloadStore.get('site'));
const store = Discourse.__container__.lookup('store:main');
return store.createRecord('site', PreloadStore.get('site'));
},
create() {
const result = this._super.apply(this, arguments);
const store = result.store;
if (result.categories) {
result.categoriesById = {};
result.categories = _.map(result.categories, function(c) {
return result.categoriesById[c.id] = Discourse.Category.create(c);
});
result.categories = _.map(result.categories, c => result.categoriesById[c.id] = store.createRecord('category', c));
// Associate the categories with their parents
result.categories.forEach(function (c) {
result.categories.forEach(c => {
if (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) {
result.trustLevels = result.trust_levels.map(function (tl) {
return Discourse.TrustLevel.create(tl);
});
result.trustLevels = result.trust_levels.map(tl => Discourse.TrustLevel.create(tl));
delete result.trust_levels;
}
if (result.post_action_types) {
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);
result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType;
@ -130,7 +129,7 @@ Site.reopenClass(Singleton, {
if (result.topic_flag_types) {
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);
result.topicFlagByIdLookup.set("action" + p.id, actionType);
return actionType;
@ -138,16 +137,14 @@ Site.reopenClass(Singleton, {
}
if (result.archetypes) {
result.archetypes = _.map(result.archetypes,function(a) {
result.archetypes = _.map(result.archetypes, a => {
a.site = result;
return Archetype.create(a);
});
}
if (result.user_fields) {
result.user_fields = result.user_fields.map(function(uf) {
return Ember.Object.create(uf);
});
result.user_fields = result.user_fields.map(uf => Ember.Object.create(uf));
}
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
// refresh it in the background.
findStale(type, findArgs) {
const stale = this.adapterFor(type).findStale(this, type, findArgs);
findStale(type, findArgs, opts) {
const stale = this.adapterFor(type).findStale(this, type, findArgs, opts);
if (stale.hasResults) {
stale.results = this._hydrateFindResults(stale.results, type, findArgs);
}
stale.refresh = () => this.find(type, findArgs);
stale.refresh = () => this.find(type, findArgs, opts);
return stale;
},
find(type, findArgs) {
return this.adapterFor(type).find(this, type, findArgs).then((result) => {
return this._hydrateFindResults(result, type, findArgs);
find(type, findArgs, opts) {
return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => {
return this._hydrateFindResults(result, type, findArgs, opts);
});
},
@ -157,6 +157,10 @@ export default Ember.Object.extend({
obj.__type = type;
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 model = klass.create(obj);

View File

@ -147,9 +147,6 @@ TopicList.reopenClass({
json.per_page = json.topic_list.per_page;
json.topics = topicsFrom(json, store);
if (json.topic_list.filtered_category) {
json.category = Discourse.Category.create(json.topic_list.filtered_category);
}
return json;
},
@ -163,10 +160,9 @@ TopicList.reopenClass({
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) {
const hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; });
list.set('hideCategory', hideCategory);
list.set('hideCategory', category && !category.get("has_children"));
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

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