mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-04-25 13:33:51 -04:00
Merge branch 'master' into signup-cta
Conflicts: app/assets/javascripts/discourse/lib/key-value-store.js.es6
This commit is contained in:
commit
6be78861ca
347 changed files with 4853 additions and 2739 deletions
CONTRIBUTING.mdGemfileGemfile.lock
app/assets/javascripts
admin
components
controllers
routes
templates
discourse
components
actions-summary.js.es6auto-close-form.js.es6autofocus-text-field.js.es6avatar-uploader.js.es6categories-admin-dropdown.js.es6category-chooser.js.es6create-topics-notice.js.es6d-button.js.es6d-link.js.es6dropdown-button.js.es6edit-category-general.js.es6edit-category-security.js.es6hamburger-menu.js.es6header-dropdown.js.es6menu-panel.js.es6nav-item.js.es6navigation-bar.js.es6navigation-item.js.es6notification-item.js.es6number-field.js.es6popup-input-tip.js.es6post-gap.js.es6search-menu.js.es6search-text-field.js.es6text-field.js.es6time-gap.js.es6user-menu.js.es6user-selector.js.es6who-liked.js.es6
controllers
avatar-selector.js.es6change-owner.js.es6change-timestamp.js.es6composer.js.es6
discovery
edit-topic-auto-close.js.es6full-page-search.js.es6header.js.es6navigation
preferences.js.es6reorder-categories.js.es6static.js.es6topic-entrance.js.es6topic.js.es6user-card.js.es6user.js.es6dialects
helpers
initializers
lib
autocomplete.js.es6avatar-template.js.es6binary-search.js.es6
emoji
intercept-click.js.es6key-value-store.js.es6keyboard-shortcuts.js.es6safari-hacks.js.es6screen-track.js.es6search.js.es6url.js.es6user-search.js.es6utilities.jsmixins
bulk-topic-selection.js.es6load-more.js.es6modal-functionality.js.es6scrolling.js.es6stale-local-storage.js.es6
models
category-list.js.es6category.js.es6category_list.jscomposer.js.es6nav-item.js.es6post-stream.js.es6post.js.es6rest.js.es6site.js.es6store.js.es6topic-list.js.es6topic-tracking-state.js.es6user-action.js.es6user.js.es6
pre-initializers
134
CONTRIBUTING.md
134
CONTRIBUTING.md
|
@ -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!*
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -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
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
{{i18n "admin.plugins.not_enabled"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{i18n "admin.plugins.cant_disable"}}
|
||||
{{i18n "admin.plugins.is_enabled"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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}) + " " + result;
|
||||
}
|
||||
|
||||
result += " <span class='topic-count'>× " + category.get('topic_count') + "</span>";
|
||||
result += ` <span class='topic-count'>× ${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 ? '…' : '') +
|
||||
'</div>';
|
||||
result += `<div class="category-desc">${description.substr(0, 200)}${description.length > 200 ? '…' : ''}</div>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -7,16 +7,24 @@ export default buildCategoryPanel('security', {
|
|||
|
||||
actions: {
|
||||
editPermissions() {
|
||||
this.set('editingPermissions', true);
|
||||
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) {
|
||||
this.get('category').removePermission(permission);
|
||||
if (!this.get('category.is_special')) {
|
||||
this.get('category').removePermission(permission);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
$(window).on('scroll.discourse-menu-panel', () => this.performLayout());
|
||||
|
||||
// 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());
|
||||
if (contentHeight !== this._lastHeight) { this.performLayout(); }
|
||||
this._lastHeight = contentHeight;
|
||||
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();
|
||||
}
|
||||
|
@ -178,7 +191,7 @@ export default Ember.Component.extend({
|
|||
$('body').off('keydown.discourse-menu-panel');
|
||||
$('html').off('click.close-menu-panel');
|
||||
$(window).off('resize.discourse-menu-panel');
|
||||
$(window).off('scroll.discourse-menu-panel');
|
||||
$(window).off('scroll.discourse-menu-panel');
|
||||
},
|
||||
|
||||
hide() {
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) : "";
|
||||
}
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
}.property('placeholderKey')
|
||||
@computed("placeholderKey")
|
||||
placeholder(placeholderKey) {
|
||||
return placeholderKey ? I18n.t(placeholderKey) : "";
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>";
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')) &&
|
||||
|
|
|
@ -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'); } );
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,8 +17,23 @@ 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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
}
|
29
app/assets/javascripts/discourse/lib/binary-search.js.es6
Normal file
29
app/assets/javascripts/discourse/lib/binary-search.js.es6
Normal 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);
|
||||
}
|
|
@ -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 }];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
32
app/assets/javascripts/discourse/lib/intercept-click.js.es6
Normal file
32
app/assets/javascripts/discourse/lib/intercept-click.js.es6
Normal 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;
|
||||
}
|
|
@ -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) {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
60
app/assets/javascripts/discourse/models/category-list.js.es6
Normal file
60
app/assets/javascripts/discourse/models/category-list.js.es6
Normal 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;
|
|
@ -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){
|
||||
availableGroups.removeObject(elem.group_name);
|
||||
return {
|
||||
group_name: elem.group_name,
|
||||
permission: Discourse.PermissionType.create({id: elem.permission_type})
|
||||
};
|
||||
})));
|
||||
|
||||
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'),
|
||||
slug = Em.get(category, 'slug');
|
||||
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;
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
|
@ -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 (category) {
|
||||
const topicTemplate = category.get('topic_template');
|
||||
if (!Ember.isEmpty(topicTemplate)) {
|
||||
this.set('reply', topicTemplate);
|
||||
// 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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
}.observes('categoryId'),
|
||||
if (!Ember.isEmpty(reply)) { return; }
|
||||
const category = this.site.categories.findProperty('id', categoryId);
|
||||
if (category) {
|
||||
this.set('reply', category.get('topic_template') || "");
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
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 || {};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -5,18 +5,13 @@ 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) {
|
||||
const lastDate = p2.get('created_at');
|
||||
if (lastDate) {
|
||||
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
|
||||
const days = Math.round(delta / (1000 * 60 * 60 * 24));
|
||||
if (date && p2) {
|
||||
const lastDate = p2.get('created_at');
|
||||
if (lastDate) {
|
||||
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
|
||||
const days = Math.round(delta / (1000 * 60 * 60 * 24));
|
||||
|
||||
p1.set('daysSincePrevious', days);
|
||||
}
|
||||
}
|
||||
p1.set('daysSincePrevious', days);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,14 +276,13 @@ const PostStream = RestModel.extend({
|
|||
// Fill in a gap of posts after a particular post
|
||||
fillGapAfter(post, gap) {
|
||||
const postId = post.get('id'),
|
||||
stream = this.get('stream'),
|
||||
idx = stream.indexOf(postId),
|
||||
self = this;
|
||||
stream = this.get('stream'),
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = [],
|
||||
remaining = {};
|
||||
@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);
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
@ -154,7 +162,7 @@ const TopicTrackingState = Discourse.Model.extend({
|
|||
|
||||
sync(list, filter) {
|
||||
const tracker = this,
|
||||
states = tracker.states;
|
||||
states = tracker.states;
|
||||
|
||||
if (!list || !list.topics) { return; }
|
||||
|
||||
|
@ -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,33 +235,28 @@ 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;})
|
||||
.value()
|
||||
.length;
|
||||
},
|
||||
|
||||
tooManyTracked() {
|
||||
return this.initialStatesLength >= Discourse.SiteSettings.max_tracked_new_unread;
|
||||
.where(isNew)
|
||||
.where(topic => topic.category_id === category_id || !category_id)
|
||||
.value()
|
||||
.length;
|
||||
},
|
||||
|
||||
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;})
|
||||
.value()
|
||||
.length;
|
||||
.where(isUnread)
|
||||
.where(topic => topic.category_id === category_id || !category_id)
|
||||
.value()
|
||||
.length;
|
||||
},
|
||||
|
||||
countCategory(category_id) {
|
||||
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue