Version bump

This commit is contained in:
Neil Lalonde 2015-09-16 11:33:24 -04:00
commit ad8f2cbed1
267 changed files with 3449 additions and 3407 deletions

View File

@ -7,6 +7,7 @@ app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/defer/html-sanitizer-bundle.js
app/assets/javascripts/discourse/lib/Markdown.Editor.js
app/assets/javascripts/ember-addons
jsapp/lib/Markdown.Editor.js
lib/javascripts/locale/
lib/javascripts/messageformat.js

View File

@ -90,10 +90,7 @@
"no-undef": 2,
"no-unused-vars": 2,
"no-with": 2,
"semi": [
0,
"never"
],
"semi": 2,
"strict": 0,
"valid-typeof": 2,
"wrap-iife": [

View File

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

View File

@ -63,7 +63,8 @@ gem 'email_reply_parser'
# note: for image_optim to correctly work you need to follow
# https://github.com/toy/image_optim
gem 'image_optim'
# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade
gem 'image_optim', '0.20.2'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'

View File

@ -209,7 +209,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.24)
onebox (1.5.25)
moneta (~> 0.8)
multi_json (~> 1.11)
mustache
@ -422,7 +422,7 @@ DEPENDENCIES
highline
hiredis
htmlentities
image_optim
image_optim (= 0.20.2)
librarian (>= 0.0.25)
listen (= 0.7.3)
logster
@ -488,3 +488,6 @@ DEPENDENCIES
uglifier
unf
unicorn
BUNDLED WITH
1.10.6

View File

@ -13,7 +13,7 @@ export default Ember.Controller.extend({
}.property('problems'),
thereWereProblems: function() {
if(!Discourse.User.currentProp('admin')) { return false }
if(!Discourse.User.currentProp('admin')) { return false; }
if( this.get('foundProblems') ) {
this.set('hadProblems', true);
return true;

View File

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

View File

@ -1,3 +1,6 @@
import { exportEntity } from 'discourse/lib/export-csv';
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.Controller.extend({
viewMode: 'table',
viewingTable: Em.computed.equal('viewMode', 'table'),
@ -30,6 +33,15 @@ export default Ember.Controller.extend({
viewAsBarChart() {
this.set('viewMode', 'barChart');
},
exportCsv() {
exportEntity('report', {
name: this.get("model.type"),
start_date: this.get('startDate'),
end_date: this.get('endDate'),
category_id: this.get('categoryId') === 'all' ? undefined : this.get('categoryId')
}).then(outputExportResult);
}
}
});

View File

@ -56,8 +56,8 @@ export default Ember.Controller.extend({
saveAll: function(){
var self = this;
var items = this.get('workingCopy');
var groupIds = items.map(function(i){return i.get("id") || -1});
var names = items.map(function(i){return i.get("name")});
var groupIds = items.map(function(i){return i.get("id") || -1;});
var names = items.map(function(i){return i.get("name");});
Discourse.ajax('/admin/badges/badge_groupings',{
data: {ids: groupIds, names: names},

View File

@ -1,4 +1,5 @@
<label>
{{input type="checkbox" checked=enabled}}
{{{unbound setting.description}}}
{{setting-validation-message message=validationMessage}}
</label>

View File

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

View File

@ -5,6 +5,7 @@
{{i18n 'admin.dashboard.reports.end_date'}} {{input type="date" value=endDate}}
{{combo-box valueAttribute="value" content=categoryOptions value=categoryId}}
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
</div>
<div class='view-options'>

View File

@ -152,10 +152,10 @@ function proxyDep(propName, moduleFunc, msg) {
});
}
proxyDep('computed', function() { return require('discourse/lib/computed') });
proxyDep('Formatter', function() { return require('discourse/lib/formatter') });
proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default });
proxyDep('URL', function() { return require('discourse/lib/url').default });
proxyDep('Quote', function() { return require('discourse/lib/quote').default });
proxyDep('debounce', function() { return require('discourse/lib/debounce').default });
proxyDep('View', function() { return Ember.View }, "Use `Ember.View` instead");
proxyDep('computed', function() { return require('discourse/lib/computed'); });
proxyDep('Formatter', function() { return require('discourse/lib/formatter'); });
proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default; });
proxyDep('URL', function() { return require('discourse/lib/url').default; });
proxyDep('Quote', function() { return require('discourse/lib/quote').default; });
proxyDep('debounce', function() { return require('discourse/lib/debounce').default; });
proxyDep('View', function() { return Ember.View; }, "Use `Ember.View` instead");

View File

@ -2,8 +2,8 @@ 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,
@computed("limited")
autoCloseUnits(limited) {
@ -19,15 +19,14 @@ export default Ember.Component.extend({
@observes("autoCloseTime", "limited")
_updateAutoCloseValid() {
const autoCloseTime = this.get("autoCloseTime");
const limited = this.get("limited");
var isValid = this._isAutoCloseValid(autoCloseTime, limited);
const limited = this.get("limited"),
autoCloseTime = this.get("autoCloseTime"),
isValid = this._isAutoCloseValid(autoCloseTime, limited);
this.set("autoCloseValid", isValid);
},
_isAutoCloseValid(autoCloseTime, limited) {
var t = (autoCloseTime || "").toString().trim();
const t = (autoCloseTime || "").toString().trim();
if (t.length === 0) {
// "empty" is always valid
return true;

View File

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ export default Ember.Component.extend({
if (color) {
var style = "";
if (color) { style += "background-color: #" + color + ";" }
if (color) { style += "background-color: #" + color + ";"; }
return style.htmlSafe();
}
}

View File

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

View File

@ -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);
}
},
}
});

View File

@ -25,7 +25,7 @@ export default Ember.Component.extend({
}
return groups.filter(function(group){
return !selectedGroups.any(function(s){return s === group.name});
return !selectedGroups.any(function(s){return s === group.name;});
});
});
},

View File

@ -2,6 +2,12 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: ['hamburger-panel'],
@computed('currentUser.read_faq')
prioritizeFaq(readFaq) {
// If it's a custom FAQ never prioritize it
return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq;
},
@computed()
showKeyboardShortcuts() {
return !Discourse.Mobile.mobileView && !this.capabilities.touch;

View File

@ -28,11 +28,9 @@ export default Ember.Component.extend({
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", height: 'auto' });
// 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
const fullHeight = parseInt($window.height());
@ -56,7 +54,7 @@ export default Ember.Component.extend({
}
$panelBody.height('100%');
this.$().css({ left: "auto", top: (menuTop - 2) + "px", height });
this.$().css({ top: menuTop + "px", height });
$('body').removeClass('drop-down-visible');
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
import { on } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNameBindings: ["visible::hidden", ":popup-menu"],
@on('didInsertElement')
_setup() {
this.appEvents.on("popup-menu:open", this, "_changeLocation");
$('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => {
const $target = $(e.target);
if ($target.is("button") || this.$().has($target).length === 0) {
this.sendAction('hide');
}
});
},
@on('willDestroyElement')
_cleanup() {
$('html').off(`mouseup.popup-menu-${this.get('elementId')}`);
this.appEvents.off("popup-menu:open", this, "_changeLocation");
},
_changeLocation(location) {
const $this = this.$();
switch (location.position) {
case "absolute": {
$this.css({
position: "absolute",
top: location.top - $this.innerHeight() + 5,
left: location.left,
});
break;
}
case "fixed": {
$this.css({
position: "fixed",
top: location.top,
left: location.left - $this.innerWidth(),
});
break;
}
}
}
});

View File

@ -369,13 +369,13 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
unhidePostIcon = iconHTML('eye'),
unhidePostText = I18n.t('post.controls.unhide');
const html = '<div class="post-admin-menu">' +
const html = '<div class="post-admin-menu popup-menu">' +
'<h3>' + I18n.t('admin_title') + '</h3>' +
'<ul>' +
'<li class="btn btn-admin" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
(Discourse.User.currentProp('staff') ? '<li class="btn btn-admin" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
'<li class="btn btn-admin" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
(post.hidden ? '<li class="btn btn-admin" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
'<li class="btn" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
'</ul>' +
'</div>';

View File

@ -1,4 +1,4 @@
import {searchForTerm, searchContextDescription} from 'discourse/lib/search';
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';
@ -61,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 {
@ -134,7 +134,7 @@ export default Ember.Component.extend({
},
showedSearch() {
$('#search-term').focus();
$('#search-term').focus().select();
},
showSearchHelp() {
@ -154,8 +154,7 @@ export default Ember.Component.extend({
},
keyDown(e) {
const term = this.get('searchService.term');
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
this.set('visible', false);
this.send('fullSearch');
}

View File

@ -1,9 +1,19 @@
import computed from 'ember-addons/ember-computed-decorators';
import { on } from 'ember-addons/ember-computed-decorators';
import TextField from 'discourse/components/text-field';
export default TextField.extend({
@computed('searchService.searchContextEnabled')
placeholder: function(searchContextEnabled) {
placeholder(searchContextEnabled) {
return searchContextEnabled ? "" : I18n.t('search.title');
},
@on("didInsertElement")
becomeFocused() {
if (!this.get('hasAutofocus')) { return; }
// iOS is crazy, without this we will not be
// at the top of the page
$(window).scrollTop(0);
this.$().focus();
}
});

View File

@ -0,0 +1,22 @@
import DButton from 'discourse/components/d-button';
export default DButton.extend({
click() {
const $target = this.$(),
position = $target.position(),
width = $target.innerWidth(),
loc = {
position: this.get('position') || "fixed",
left: position.left + width,
top: position.top
};
// TODO views/topic-footer-buttons is instantiating this via attachViewWithArgs
// attachViewWithArgs does not set this.appEvents, it is undefined
// this is a workaround but a proper fix probably depends on either deprecation
// of attachViewClass et.el or correction of the methods to hydrate the depndencies
this.appEvents = this.appEvents || this.container.lookup('app-events:main');
this.appEvents.trigger("popup-menu:open", loc);
this.sendAction("action");
}
});

View File

@ -1,24 +0,0 @@
export default Em.Component.extend({
tagName: "button",
classNames: ["btn", "no-text", "show-topic-admin"],
attributeBindings: ["title"],
title: I18n.t("topic_admin_menu"),
render: function(buffer) {
buffer.push("<i class='fa fa-wrench'></i>");
},
click: function() {
var $target = this.$(),
position = $target.position(),
width = $target.innerWidth();
var location = {
position: "fixed",
left: position.left + width,
top: position.top,
};
this.appEvents.trigger("topic-admin-menu:open", location);
this.sendAction("show");
return false;
}
});

View File

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

View File

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

View File

@ -11,4 +11,4 @@ export default Ember.Component.extend({
list.removeObject(id);
}
}.observes('selected')
})
});

View File

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

View File

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

View File

@ -3,6 +3,7 @@ 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 || {};
@ -54,6 +55,7 @@ export default Ember.Controller.extend({
similarTopics: null,
similarTopicsMessage: null,
lastSimilaritySearch: null,
optionsVisible: false,
topic: null,
@ -64,6 +66,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; }
@ -77,6 +85,20 @@ export default Ember.Controller.extend({
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
actions: {
toggleWhisper() {
this.toggleProperty('model.whisper');
},
showOptions(loc) {
this.appEvents.trigger('popup-menu:open', loc);
this.set('optionsVisible', true);
},
hideOptions() {
this.set('optionsVisible', false);
},
// Toggle the reply view
toggle() {
this.toggle();
@ -132,7 +154,6 @@ export default Ember.Controller.extend({
},
hitEsc() {
const messages = this.get('controllers.composer-messages.model');
if (messages.length) {
messages.popObject();

View File

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

View File

@ -1,4 +1,4 @@
import { translateResults, searchContextDescription, getSearchKey } from "discourse/lib/search";
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';
@ -13,13 +13,18 @@ export default Ember.Controller.extend({
context_id: null,
context: null,
@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" )
this.set('skip_context', val ? "false" : "true" );
}
},
@ -37,7 +42,12 @@ export default Ember.Controller.extend({
@computed('q')
searchActive(q){
return q && q.length > 0;
return isValidSearchTerm(q);
},
@computed('searchTerm')
isNotValidSearchTerm(searchTerm) {
return !isValidSearchTerm(searchTerm);
},
@observes('model')
@ -90,7 +100,7 @@ export default Ember.Controller.extend({
const model = translateResults(results) || {};
router.transientCache('lastSearch', { searchKey, model }, 5);
this.set("model", model);
}).finally(() => {this._searching = false});
}).finally(() => this._searching = false);
},
actions: {
@ -106,7 +116,7 @@ export default Ember.Controller.extend({
},
clearAll() {
this.get('selected').clear()
this.get('selected').clear();
$('.fps-result input[type=checkbox]').prop('checked', false);
},
@ -129,6 +139,7 @@ export default Ember.Controller.extend({
},
search() {
if (this.get("isNotValidSearchTerm")) return;
this.search();
}
}

View File

@ -33,7 +33,7 @@ const HeaderController = Ember.Controller.extend({
var params = "";
if (context) {
params = `?context=${context.type}&context_id=${context.id}`;
params = `?context=${context.type}&context_id=${context.id}&skip_context=true`;
}
DiscourseURL.routeTo('/search' + params);

View File

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

View File

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

View File

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

View File

@ -1,66 +1,61 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy';
import binarySearch from 'discourse/lib/binary-search';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
import { on, default as 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, {
@on('init')
_fixOrder() {
this.send('fixIndices');
},
@computed("site.categories")
categoriesBuffered(categories) {
const bufProxy = Ember.ObjectProxy.extend(BufferedProxy);
return categories.map(c => bufProxy.create({ content: c }));
},
// uses propertyDidChange()
@computed('categoriesBuffered')
categoriesGrouped(cats) {
const map = {};
cats.forEach((cat) => {
const p = cat.get('position') || 0;
if (!map[p]) {
map[p] = {pos: p, cats: [cat]};
} else {
map[p].cats.push(cat);
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;
}
});
const result = [];
Object.keys(map).map(p => parseInt(p)).sort((a,b) => a-b).forEach(function(pos) {
result.push(map[pos]);
});
return result;
},
}
return false;
}.property('categoriesOrdered.@each.position'),
showApplyAll: function() {
let anyChanged = false;
this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges') });
this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges'); });
return anyChanged;
}.property('categoriesBuffered.@each.hasBufferedChanges'),
saveDisabled: Ember.computed.alias('showApplyAll'),
saveDisabled: Ember.computed.or('showApplyAll', 'showFixIndices'),
moveDir(cat, dir) {
const grouped = this.get('categoriesGrouped'),
curPos = cat.get('position'),
curGroupIdx = binarySearch(grouped, curPos, "pos"),
curGroup = grouped[curGroupIdx];
if (curGroup.cats.length === 1 && ((dir === -1 && curGroupIdx !== 0) || (dir === 1 && curGroupIdx !== (grouped.length - 1)))) {
const nextGroup = grouped[curGroupIdx + dir],
nextPos = nextGroup.pos;
cat.set('position', nextPos);
} else {
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');
}
cat.applyBufferedChanges();
Ember.run.next(this, () => {
this.propertyDidChange('categoriesGrouped');
Ember.run.schedule('afterRender', this, () => {
this.set('scrollIntoViewId', cat.get('id'));
this.trigger('scrollIntoView');
});
});
},
actions: {
@ -72,13 +67,22 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
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('categoriesGrouped');
this.propertyDidChange('categoriesBuffered');
},
saveOrder() {

View File

@ -1,12 +0,0 @@
// This controller supports the admin menu on topics
export default Ember.Controller.extend({
menuVisible: false,
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
actions: {
show: function() { this.set('menuVisible', true); },
hide: function() { this.set('menuVisible', false); }
}
});

View File

@ -19,6 +19,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
enteredAt: null,
firstPostExpanded: false,
retrying: false,
adminMenuVisible: false,
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
maxTitleLength: setting('max_topic_title_length'),
@ -93,6 +97,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}.on('init'),
actions: {
showTopicAdminMenu() {
this.set('adminMenuVisible', true);
},
hideTopicAdminMenu() {
this.set('adminMenuVisible', false);
},
deleteTopic() {
this.deleteTopic();
},

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import interceptClick from 'discourse/lib/intercept-click';
import DiscourseURL from 'discourse/lib/url';
export default {
name: "click-interceptor",
initialize() {
$('#main').on('click.discourse', 'a', interceptClick);
$(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash));
}
};

View File

@ -163,7 +163,7 @@
}
}
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString);
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString, options);
uiManager.setUndoRedoButtonStates();
var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
@ -1219,12 +1219,12 @@
}, 0);
};
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) {
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString, options) {
var inputBox = panels.input,
buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
makeSpritedButtonRow();
makeSpritedButtonRow(options);
var keyEvent = "keydown";
@ -1396,7 +1396,8 @@
return function () { method.apply(commandManager, arguments); }
}
function makeSpritedButtonRow() {
function makeSpritedButtonRow(options) {
options = options || {};
var buttonBar = panels.buttonBar;
var buttonRow = document.createElement("div");
@ -1459,17 +1460,21 @@
buttons.heading = makeButton("wmd-heading-button", getString("heading"), bindCommand("doHeading"));
buttons.hr = makeButton("wmd-hr-button", getString("hr"), bindCommand("doHorizontalRule"));
// If we have any buttons to append, do it!
if (typeof PagedownCustom != "undefined") {
var appendButtons = PagedownCustom.appendButtons
if (appendButtons && (appendButtons.length > 0)) {
for (var i=0; i< appendButtons.length; i++) {
var b = appendButtons[i];
function createExtraButtons(buttons) {
if (buttons && (buttons.length > 0)) {
for (var i=0; i< buttons.length; i++) {
var b = buttons[i];
makeButton(b.id, b.description, b.execute)
}
}
}
// If we have any buttons to append, do it!
if (typeof PagedownCustom != "undefined") {
createExtraButtons(PagedownCustom.appendButtons);
}
createExtraButtons(options.appendButtons);
//makeSpacer(3);
//buttons.undo = makeButton("wmd-undo-button", getString("undo"), null);

View File

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

View File

@ -22,4 +22,4 @@ Discourse.CensoredWords = {
}
return text;
}
}
};

View File

@ -1,7 +1,7 @@
function exportEntityByType(type, entity) {
function exportEntityByType(type, entity, args) {
return Discourse.ajax("/export_csv/export_entity.json", {
method: 'POST',
data: {entity_type: type, entity}
data: {entity_type: type, entity, args}
});
}
@ -14,6 +14,6 @@ export function exportUserArchive() {
}
export function exportEntity(entity) {
return exportEntityByType('admin', entity);
export function exportEntity(entity, args) {
return exportEntityByType('admin', entity, args);
}

View File

@ -14,7 +14,7 @@ try {
const KeyValueStore = function(ctx) {
this.context = ctx;
}
};
KeyValueStore.prototype = {
abandonLocal() {
@ -32,6 +32,7 @@ KeyValueStore.prototype = {
},
remove(key) {
if (!safeLocalStorage) { return; }
return safeLocalStorage.removeItem(this.context + key);
},

View File

@ -1,62 +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',
'g m': '/my/messages'
},
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 {
@ -65,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() {
@ -223,17 +224,11 @@ export default {
},
_bindToSelectedPost(action, binding) {
const self = this;
this.keyTrapper.bind(binding, function() {
self.sendToSelectedPost(action);
});
this.keyTrapper.bind(binding, () => this.sendToSelectedPost(action));
},
_bindToPath(path, binding) {
this.keyTrapper.bind(binding, function() {
DiscourseURL.routeTo(path);
});
_bindToPath(path, key) {
this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path));
},
_bindToClick(selector, binding) {

View File

@ -152,16 +152,16 @@ Discourse.Markdown = {
return this.markdownConverter(opts).makeHtml(raw);
},
createEditor: function(converterOptions) {
if (!converterOptions) converterOptions = {};
createEditor: function(options) {
options = options || {};
// By default we always sanitize content in the editor
converterOptions.sanitize = true;
options.sanitize = true;
var markdownConverter = Discourse.Markdown.markdownConverter(converterOptions);
var markdownConverter = Discourse.Markdown.markdownConverter(options);
var editorOptions = {
containerElement: converterOptions.containerElement,
containerElement: options.containerElement,
strings: {
bold: I18n.t("composer.bold_title") + " <strong> Ctrl+B",
boldexample: I18n.t("composer.bold_text"),
@ -197,7 +197,8 @@ Discourse.Markdown = {
redomac: I18n.t("composer.redo_title") + " - Ctrl+Shift+Z",
help: I18n.t("composer.help")
}
},
appendButtons: options.appendButtons
};
return new Markdown.Editor(markdownConverter, undefined, editorOptions);

View File

@ -1,11 +1,8 @@
function applicable() {
// CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching)
// Dolphin has a wierd user agent, rest seem a bit nitch
// This will apply hack on all iDevices
return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
navigator.userAgent.match(/Safari/g) &&
!navigator.userAgent.match(/CriOS/g) &&
!navigator.userAgent.match(/OPiOS/g);
navigator.userAgent.match(/Safari/g);
}
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
@ -17,6 +14,8 @@ function positioningWorkaround($fixedElement) {
const fixedElement = $fixedElement[0];
var done = false;
var originalScrollTop = 0;
var wasDocked;
var blurredNow = function(evt) {
if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
@ -25,8 +24,20 @@ 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 (wasDocked) {
$('body').addClass('docked');
}
if (evt) {
evt.target.removeEventListener('blur', blurred);
}
@ -50,31 +61,25 @@ function positioningWorkaround($fixedElement) {
return;
}
originalScrollTop = $(window).scrollTop();
wasDocked = $('body').hasClass('docked');
// 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();
@ -88,7 +93,11 @@ function positioningWorkaround($fixedElement) {
}
const checkForInputs = _.debounce(function(){
$fixedElement.find('button,a').each(function(){
$fixedElement.find('button,a:not(.autocomplete)').each(function(idx, elem){
if ($(elem).parents('.autocomplete').length > 0) {
return;
}
attachTouchStart(this, function(evt){
done = true;
$(document.activeElement).blur();

View File

@ -30,7 +30,7 @@ const ScreenTrack = Ember.Object.extend({
self.tick();
}, 1000));
$(window).on('scroll.screentrack', function(){self.scrolled()});
$(window).on('scroll.screentrack', function(){self.scrolled();});
}
this.set('topicId', topicId);

View File

@ -103,7 +103,15 @@ const searchContextDescription = function(type, name){
const getSearchKey = function(args){
return args.q + "|" + ((args.searchContext && args.searchContext.type) || "") + "|" +
((args.searchContext && args.searchContext.id) || "")
((args.searchContext && args.searchContext.id) || "");
};
export { searchForTerm, searchContextDescription, getSearchKey };
const isValidSearchTerm = function(searchTerm) {
if (searchTerm) {
return searchTerm.trim().length >= Discourse.SiteSettings.min_search_term_length;
} else {
return false;
}
};
export { searchForTerm, searchContextDescription, getSearchKey, isValidSearchTerm };

View File

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

View File

@ -99,7 +99,7 @@ Discourse.Utilities = {
div.innerHTML = html;
var $div = $(div);
// Find all emojis and replace with its title attribute.
$div.find('img.emoji').replaceWith(function() { return this.title });
$div.find('img.emoji').replaceWith(function() { return this.title; });
$('.clicks', $div).remove();
var text = div.textContent || div.innerText || "";
@ -215,6 +215,10 @@ Discourse.Utilities = {
}
},
getUploadPlaceholder: function(filename) {
return "[" + I18n.t("uploading_filename", { filename: filename }) + "]() ";
},
isAnImage: function(path) {
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
},

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ export default {
findStale(store, type, findArgs, opts) {
const staleResult = new StaleResult();
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs);
try {
const stored = this.keyValueStore.getItem(key);
if (stored) {
@ -24,11 +24,11 @@ export default {
},
find(store, type, findArgs, opts) {
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs);
return this._super(store, type, findArgs).then((results) => {
this.keyValueStore.setItem(key, JSON.stringify(results));
return results;
});
}
}
};

View File

@ -198,76 +198,71 @@ var _uncategorized;
Category.reopenClass({
findUncategorized: function() {
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 = 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 Category.list().find(function(c) {
return Category.slugFor(c) === slug;
});
findSingleBySlug(slug) {
return Category.list().find(c => Category.slugFor(c) === slug);
},
findById: function(id) {
findById(id) {
if (!id) { return; }
return Category.idMap()[id];
},
findByIds: function(ids){
var categories = [];
_.each(ids, function(id){
var found = 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 = Category.list(),
category;
findBySlug(slug, parentSlug) {
const categories = Category.list();
let category;
if (parentSlug) {
var parentCategory = Category.findSingleBySlug(parentSlug);
const parentCategory = Category.findSingleBySlug(parentSlug);
if (parentCategory) {
if (slug === 'none') { return parentCategory; }
category = categories.find(function(item) {
category = categories.find(item => {
return item && item.get('parentCategory') === parentCategory && Category.slugFor(item) === (parentSlug + "/" + slug);
});
}
@ -287,7 +282,7 @@ Category.reopenClass({
},
reloadById(id) {
return Discourse.ajax("/c/" + id + "/show.json");
return Discourse.ajax(`/c/${id}/show.json`);
}
});

View File

@ -3,6 +3,7 @@ import Topic from 'discourse/models/topic';
import { throwAjaxError } from 'discourse/lib/ajax-error';
import Quote from 'discourse/lib/quote';
import Draft from 'discourse/models/draft';
import computed from 'ember-addons/ember-computed-decorators';
const CLOSED = 'closed',
SAVING = 'saving',
@ -23,6 +24,7 @@ const CLOSED = 'closed',
category: 'categoryId',
topic_id: 'topic.id',
is_warning: 'isWarning',
whisper: 'whisper',
archetype: 'archetypeId',
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
@ -35,15 +37,42 @@ const CLOSED = 'closed',
};
const Composer = RestModel.extend({
_categoryId: null,
archetypes: function() {
return this.site.get('archetypes');
}.property(),
@computed
categoryId: {
get() { return this._categoryId; },
// We wrap categoryId this way so we can fire `applyTopicTemplate` with
// the previous value as well as the new value
set(categoryId) {
const oldCategoryId = this._categoryId;
if (Ember.isEmpty(categoryId)) { categoryId = null; }
this._categoryId = categoryId;
if (oldCategoryId !== categoryId) {
this.applyTopicTemplate(oldCategoryId, categoryId);
}
return categoryId;
}
},
creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
showCategoryChooser: function(){
const manyCategories = Discourse.Category.list().length > 1;
const hasOptions = this.get('archetype.hasOptions');
return !this.get('privateMessage') && (hasOptions || manyCategories);
}.property('privateMessage'),
privateMessage: function(){
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
}.property('creatingPrivateMessage', 'topic'),
@ -56,6 +85,7 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal('composeState', OPEN),
viewDraft: Em.computed.equal('composeState', DRAFT),
composeStateChanged: function() {
var oldOpen = this.get('composerOpened');
@ -339,20 +369,24 @@ const Composer = RestModel.extend({
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
},
applyTopicTemplate: function() {
applyTopicTemplate(oldCategoryId, categoryId) {
if (this.get('action') !== CREATE_TOPIC) { return; }
if (!Ember.isEmpty(this.get('reply'))) { return; }
let reply = this.get('reply');
const categoryId = this.get('categoryId');
const category = this.site.categories.find((c) => c.get('id') === categoryId);
if (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')
}
});
}

View File

@ -103,21 +103,17 @@ NavItem.reopenClass({
buildList(category, args) {
args = args || {};
if (category) { args.category = category }
var items = Discourse.SiteSettings.top_menu.split("|");
if (category) { args.category = category; }
if (args.filterMode && !_.some(items, function(i){
return i.indexOf(args.filterMode) !== -1;
})) {
let items = Discourse.SiteSettings.top_menu.split("|");
if (args.filterMode && !_.some(items, i => i.indexOf(args.filterMode) !== -1)) {
items.push(args.filterMode);
}
return items.map(function(i) {
return Discourse.NavItem.fromText(i, args);
}).filter(function(i) {
return i !== null && !(category && i.get("name").indexOf("categor") === 0);
});
return items.map(i => Discourse.NavItem.fromText(i, args))
.filter(i => i !== null && !(category && i.get("name").indexOf("categor") === 0));
}
});

View File

@ -5,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);
}
}
}

View File

@ -1,7 +1,7 @@
import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import ActionSummary from 'discourse/models/action-summary';
import { url, fmt, propertyEqual } from 'discourse/lib/computed';
import { url, propertyEqual } from 'discourse/lib/computed';
import Quote from 'discourse/lib/quote';
import computed from 'ember-addons/ember-computed-decorators';
@ -77,7 +77,6 @@ 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');

View File

@ -1,3 +1,4 @@
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';
@ -7,30 +8,30 @@ 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);
@ -40,17 +41,17 @@ const Site = RestModel.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);
@ -98,16 +99,14 @@ Site.reopenClass(Singleton, {
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] = store.createRecord('category', 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')]);
}
@ -115,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;
@ -133,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;
@ -141,16 +137,14 @@ Site.reopenClass(Singleton, {
}
if (result.archetypes) {
result.archetypes = _.map(result.archetypes,function(a) {
result.archetypes = _.map(result.archetypes, a => {
a.site = result;
return Archetype.create(a);
});
}
if (result.user_fields) {
result.user_fields = result.user_fields.map(function(uf) {
return Ember.Object.create(uf);
});
result.user_fields = result.user_fields.map(uf => Ember.Object.create(uf));
}
return result;

View File

@ -174,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);
}
}
}
@ -276,15 +276,17 @@ const TopicTrackingState = Discourse.Model.extend({
this.lookupCount("unread", category);
}
let categoryId = category ? Em.get(category, "id") : null;
let categoryName = category ? Em.get(category, "name") : null;
if (name === "new") {
return this.countNew(categoryName);
return this.countNew(categoryId);
} else if (name === "unread") {
return this.countUnread(categoryName);
return this.countUnread(categoryId);
} else {
categoryName = name.split("/")[1];
if (categoryName) {
return this.countCategory(categoryName);
return this.countCategory(categoryId);
}
}
},

View File

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

View File

@ -1,11 +1,11 @@
import { url } from 'discourse/lib/computed';
import RestModel from 'discourse/models/rest';
import avatarTemplate from 'discourse/lib/avatar-template';
import UserStream from 'discourse/models/user-stream';
import UserPostsStream from 'discourse/models/user-posts-stream';
import Singleton from 'discourse/mixins/singleton';
import { longDate } from 'discourse/lib/formatter';
import computed from 'ember-addons/ember-computed-decorators';
import { observes } from 'ember-addons/ember-computed-decorators';
import Badge from 'discourse/models/badge';
import UserBadge from 'discourse/models/user-badge';
@ -18,13 +18,15 @@ const User = RestModel.extend({
hasNotPosted: Em.computed.not("hasPosted"),
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
stream: function() {
@computed()
stream() {
return UserStream.create({ user: this });
}.property(),
},
postsStream: function() {
@computed()
postsStream() {
return UserPostsStream.create({ user: this });
}.property(),
},
staff: Em.computed.or('admin', 'moderator'),
@ -32,27 +34,22 @@ const User = RestModel.extend({
return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'});
},
searchContext: function() {
@computed("username_lower")
searchContext(username) {
return {
type: 'user',
id: this.get('username_lower'),
id: username,
user: this
};
}.property('username_lower'),
},
/**
This user's display name. Returns the name if possible, otherwise returns the
username.
@property displayName
@type {String}
**/
displayName: function() {
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) {
return this.get('name');
@computed("username", "name")
displayName(username, name) {
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
return name;
}
return this.get('username');
}.property('username', 'name'),
return username;
},
@computed('profile_background')
profileBackground(bgUrl) {
@ -60,38 +57,23 @@ const User = RestModel.extend({
return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe();
},
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),
@ -100,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',
@ -179,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;
});
@ -192,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') },
@ -219,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) {
@ -294,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) {
@ -311,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')) {
@ -398,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 () {});
});
}
});
@ -426,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);
@ -441,56 +359,38 @@ User.reopenClass(Singleton, {
return null;
},
/**
Checks if given username is valid for this email address
@method checkUsername
@param {String} username A username to check
@param {String} email An email address to check
@param {Number} forUserId user id - provide when changing username
**/
checkUsername: function(username, email, forUserId) {
checkUsername(username, email, for_user_id) {
return Discourse.ajax('/users/check_username', {
data: { username: username, email: email, for_user_id: forUserId }
data: { username, email, for_user_id }
});
},
/**
Groups the user's statistics
@method groupStats
@param {Array} stats Given stats
@returns {Object}
**/
groupStats: function(stats) {
var responses = Discourse.UserActionStat.create({
groupStats(stats) {
const responses = Discourse.UserActionStat.create({
count: 0,
action_type: Discourse.UserAction.TYPES.replies
});
stats.filterProperty('isResponse').forEach(function (stat) {
stats.filterProperty('isResponse').forEach(stat => {
responses.set('count', responses.get('count') + stat.get('count'));
});
var result = Em.A();
const result = Em.A();
result.pushObjects(stats.rejectProperty('isResponse'));
var insertAt = 0;
result.forEach(function(item, index){
if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){
let insertAt = 0;
result.forEach((item, index) => {
if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) {
insertAt = index + 1;
}
});
if(responses.count > 0) {
if (responses.count > 0) {
result.insertAt(insertAt, responses);
}
return(result);
return result;
},
/**
Creates a new account
**/
createAccount: function(attrs) {
createAccount(attrs) {
return Discourse.ajax("/users", {
data: {
name: attrs.accountName,

View File

@ -34,6 +34,7 @@ export default {
app.register('message-bus:main', window.MessageBus, { instantiate: false });
injectAll(app, 'messageBus');
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
app.register('topic-tracking-state:main', TopicTrackingState.current(), { instantiate: false });
injectAll(app, 'topicTrackingState');
@ -50,7 +51,6 @@ export default {
app.register('session:main', Session.current(), { instantiate: false });
injectAll(app, 'session');
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
inject(app, 'currentUser', 'component', 'route', 'controller');
app.register('location:discourse-location', DiscourseLocation);

View File

@ -1,15 +1,15 @@
import { queryParams, filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route';
// A helper function to create a category route with parameters
export default function(filter, params) {
export default (filter, params) => {
return Discourse.Route.extend({
queryParams: queryParams,
model: function(modelParams) {
model(modelParams) {
return Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug);
},
afterModel: function(model, transition) {
afterModel(model, transition) {
if (!model) {
this.replaceWith('/404');
return;
@ -20,9 +20,9 @@ export default function(filter, params) {
this._retrieveTopicList(model, transition)]);
},
_setupNavigation: function(model) {
var noSubcategories = params && !!params.no_subcategories,
filterMode = "c/" + Discourse.Category.slugFor(model) + (noSubcategories ? "/none" : "") + "/l/" + filter;
_setupNavigation(model) {
const noSubcategories = params && !!params.no_subcategories,
filterMode = `c/${Discourse.Category.slugFor(model)}${noSubcategories ? "/none" : ""}/l/${filter}`;
this.controllerFor('navigation/category').setProperties({
category: model,
@ -32,42 +32,38 @@ export default function(filter, params) {
});
},
_createSubcategoryList: function(model) {
_createSubcategoryList(model) {
this._categoryList = null;
if (Em.isNone(model.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
var self = this;
return Discourse.CategoryList.listForParent(this.store, model).then(function(list) {
self._categoryList = list;
});
return Discourse.CategoryList.listForParent(this.store, model)
.then(list => this._categoryList = list);
}
// If we're not loading a subcategory list just resolve
return Em.RSVP.resolve();
},
_retrieveTopicList: function(model, transition) {
var listFilter = "c/" + Discourse.Category.slugFor(model) + "/l/" + filter,
self = this;
_retrieveTopicList(model, transition) {
const listFilter = `c/${Discourse.Category.slugFor(model)}/l/${filter}`,
findOpts = filterQueryParams(transition.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
var findOpts = filterQueryParams(transition.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(function(list) {
return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(list => {
Discourse.TopicList.hideUniformCategory(list, model);
self.set('topics', list);
this.set('topics', list);
});
},
titleToken: function() {
var filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0}),
model = this.currentModel;
titleToken() {
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', { count: 0 }),
model = this.currentModel;
return I18n.t('filters.with_category', { filter: filterText, category: model.get('name') });
},
setupController: function(controller, model) {
var topics = this.get('topics'),
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
setupController(controller, model) {
const topics = this.get('topics'),
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic'));
this.controllerFor('discovery/topics').setProperties({
@ -87,7 +83,7 @@ export default function(filter, params) {
this.openTopicDraft(topics);
},
renderTemplate: function() {
renderTemplate() {
this.render('navigation/category', { outlet: 'navigation-bar' });
if (this._categoryList) {
@ -96,15 +92,15 @@ export default function(filter, params) {
this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' });
},
deactivate: function() {
deactivate() {
this._super();
this.searchService.set('searchContext', null);
},
actions: {
setNotification: function(notification_level){
setNotification(notification_level) {
this.currentModel.setNotification(notification_level);
}
}
});
}
};

View File

@ -16,7 +16,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
// if default page is categories
PreloadStore.remove("topic_list");
return Discourse.CategoryList.list(this.store, 'categories').then((list) => {
return Discourse.CategoryList.list(this.store, 'categories').then(list => {
const tracking = this.topicTrackingState;
if (tracking) {
tracking.sync(list, "categories");
@ -34,9 +34,10 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
setupController(controller, model) {
controller.set("model", model);
// Only show either the Create Category or Create Topic button
this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category"));
this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category"));
this.controllerFor("navigation/categories").setProperties({
canCreateCategory: model.get("can_create_category"),
canCreateTopic: model.get("can_create_topic"),
});
this.openTopicDraft(model);
},

View File

@ -1,7 +1,7 @@
import { translateResults, getSearchKey } from "discourse/lib/search";
import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
export default Discourse.Route.extend({
queryParams: { q: {}, context_id: {}, context: {} },
queryParams: { q: {}, context_id: {}, context: {}, skip_context: {} },
model(params) {
const router = Discourse.__container__.lookup('router:main');
@ -11,7 +11,7 @@ export default Discourse.Route.extend({
args.search_context = {
type: params.context,
id: params.context_id
}
};
}
const searchKey = getSearchKey(args);
@ -23,7 +23,7 @@ export default Discourse.Route.extend({
}
return PreloadStore.getAndRemove("search", function() {
if (params.q && params.q.length > 2) {
if (isValidSearchTerm(params.q)) {
return Discourse.ajax("/search", { data: args });
} else {
return null;

View File

@ -18,50 +18,52 @@ export default RestrictedUserRoute.extend({
showModal('avatar-selector');
// all the properties needed for displaying the avatar selector modal
const controller = this.controllerFor('avatar-selector'),
props = this.modelFor('user').getProperties(
const props = this.modelFor('user').getProperties(
'id',
'email',
'username',
'uploaded_avatar_id',
'avatar_template',
'system_avatar_template',
'gravatar_avatar_template',
'custom_avatar_template',
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
);
switch (props.uploaded_avatar_id) {
case props.system_avatar_upload_id:
switch (props.avatar_template) {
case props.system_avatar_template:
props.selected = "system";
break;
case props.gravatar_avatar_upload_id:
case props.gravatar_avatar_template:
props.selected = "gravatar";
break;
default:
props.selected = "uploaded";
}
controller.setProperties(props);
this.controllerFor('avatar-selector').setProperties(props);
},
saveAvatarSelection() {
const user = this.modelFor('user'),
avatarSelector = this.controllerFor('avatar-selector');
controller = this.controllerFor('avatar-selector'),
selectedUploadId = controller.get("selectedUploadId"),
selectedAvatarTemplate = controller.get("selectedAvatarTemplate"),
type = controller.get("selected");
// sends the information to the server if it has changed
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
user.pickAvatar(avatarSelector.get('selectedUploadId'))
.then(() => {
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
});
}
user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate)
.then(() => {
user.setProperties(controller.getProperties(
'system_avatar_template',
'gravatar_avatar_template',
'custom_avatar_template'
));
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
});
// saves the data back
avatarSelector.send('closeModal');
controller.send('closeModal');
},
}

View File

@ -41,10 +41,6 @@ const TopicRoute = Discourse.Route.extend({
actions: {
showTopicAdminMenu() {
this.controllerFor("topic-admin-menu").send("show");
},
showFlags(model) {
showModal('flag', { model });
this.controllerFor('flag').setProperties({ selected: null });
@ -56,7 +52,7 @@ const TopicRoute = Discourse.Route.extend({
},
showAutoClose() {
showModal('edit-topic-auto-close', { model: this.modelFor('topic'), title: 'topic.auto_close_title' });
showModal('edit-topic-auto-close', { model: this.modelFor('topic') });
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
},
@ -213,7 +209,6 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false });
this.searchService.set('searchContext', model.get('searchContext'));
this.controllerFor('topic-admin-menu').set('model', model);
this.controllerFor('composer').set('topic', model);
this.topicTrackingState.trackIncoming('all');

View File

@ -61,7 +61,7 @@ export default Discourse.Route.extend({
setupController(controller, user) {
controller.set('model', user);
this.searchService.set('searchContext', user.get('searchContext'))
this.searchService.set('searchContext', user.get('searchContext'));
},
activate() {
@ -77,7 +77,7 @@ export default Discourse.Route.extend({
this.messageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));
// Remove the search context
this.searchService.set('searchContext', null)
this.searchService.set('searchContext', null);
}
});

View File

@ -12,7 +12,7 @@
</div>
<div>
<label>
{{input type="checkbox" name="autoCloseBasedOnLastPost" checked=autoCloseBasedOnLastPost}}
{{input type="checkbox" checked=autoCloseBasedOnLastPost}}
{{i18n 'composer.auto_close.based_on_last_post'}}
</label>
</div>

View File

@ -1,4 +1,7 @@
<section class='field'>
{{#if category.is_special}}
<p class="warning">{{i18n 'category.special_warning'}}</p>
{{/if}}
<ul class='permission-list'>
{{#each category.permissions as |p|}}
<li>
@ -16,6 +19,8 @@
{{view 'select' class="permission-selector" optionValuePath="content.id" optionLabelPath="content.description" content=category.availablePermissions value=selectedPermission}}
<button {{action "addPermission" selectedGroup selectedPermission}} class="btn btn-small">{{i18n 'category.add_permission'}}</button>
{{else}}
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
{{#unless category.is_special}}
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
{{/unless}}
{{/if}}
</section>

View File

@ -1,14 +1,14 @@
{{#menu-panel visible=visible}}
{{#unless currentUser.read_faq}}
{{#if prioritizeFaq}}
{{#menu-links}}
<li>
<li class='heading'>
{{#d-link path=faqUrl class="faq-link"}}
{{i18n "faq"}}
<span class='new'>{{i18n "new_item"}}</span>
<span class='badge badge-notification'>{{i18n "new_item"}}</span>
{{/d-link}}
</li>
{{/menu-links}}
{{/unless}}
{{/if}}
{{#if currentUser.staff}}
{{#menu-links}}
@ -32,7 +32,9 @@
{{/d-link}}
</li>
{{/if}}
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
{{#if currentUser.admin}}
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
{{/if}}
{{plugin-outlet "hamburger-admin"}}
{{/menu-links}}
@ -81,9 +83,9 @@
{{#menu-links omitRule="true"}}
<li>{{d-link route="about" class="about-link" label="about.simple_title"}}</li>
{{#if currentUser.read_faq}}
{{#unless prioritizeFaq}}
<li>{{d-link path=faqUrl class="faq-link" label="faq"}}</li>
{{/if}}
{{/unless}}
{{#if showKeyboardShortcuts}}
<li>{{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}</li>

View File

@ -0,0 +1,4 @@
<h3>{{i18n title}}</h3>
<ul>
{{yield}}
</ul>

View File

@ -23,7 +23,7 @@
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
</button>
{{else}}
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}</div></a>
{{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
{{/if}}
{{/each}}

View File

@ -8,7 +8,6 @@
{{/if}}
<li class='glyphs'>
{{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}}
{{log siteSettings}}
{{#if siteSettings.enable_private_messages}}
{{d-link path=messagesPath title="user.private_messages" icon="envelope"}}
{{/if}}

View File

@ -1,8 +1,17 @@
{{#if visible}}
<div class='contents'>
{{#if currentUser.staff}}
{{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}}
<li>
{{d-button action="toggleWhisper" icon="eye-slash" label="composer.toggle_whisper"}}
</li>
{{/popup-menu}}
{{/if}}
{{render "composer-messages"}}
<div class='control'>
<a href class='toggler' {{action "toggle" bubbles=false}} title='{{i18n 'composer.toggler'}}'></a>
<a href class='toggler' {{action "toggle" bubbles=false}} title={{i18n 'composer.toggler'}}></a>
{{#if model.viewOpen}}
<div class='control-row reply-area'>
@ -11,6 +20,10 @@
<div class='reply-to'>
{{{model.actionTitle}}}
{{#if model.whisper}}
<span class='whisper'>({{i18n "composer.whisper"}})</span>
{{/if}}
{{#if canEdit}}
{{#if showEditReason}}
<div class="edit-reason-input">
@ -48,7 +61,7 @@
{{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}}
</div>
{{#unless model.privateMessage}}
{{#if model.showCategoryChooser}}
<div class="category-input">
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}}
{{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}}
@ -57,9 +70,10 @@
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
{{/if}}
{{render "additional-composer-buttons" model}}
{{/unless}}
{{/if}}
</div>
{{/if}}
{{plugin-outlet "composer-fields"}}
</div>

View File

@ -1,6 +1,6 @@
<div class="search row clearfix">
{{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}}
{{d-button action="search" icon="search" class="btn-primary"}}
{{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search" hasAutofocus=hasAutofocus}}
{{d-button action="search" icon="search" class="btn-primary" disabled=isNotValidSearchTerm}}
{{#if canBulkSelect}}
{{#if model.posts}}
{{d-button icon="list" class="bulk-select" title="topics.bulk.toggle" action="toggleBulkSelect"}}

View File

@ -1,8 +1,3 @@
{{plugin-outlet "header-before-dropdowns"}}
{{user-menu visible=userMenuVisible logoutAction="logout"}}
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
{{search-menu visible=searchVisible}}
<div class='wrap'>
<div class='contents clearfix'>
{{home-logo minimized=showExtraInfo}}
@ -54,6 +49,10 @@
{{/header-dropdown}}
{{/if}}
</ul>
{{plugin-outlet "header-before-dropdowns"}}
{{user-menu visible=userMenuVisible logoutAction="logout"}}
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
{{search-menu visible=searchVisible}}
</div>
{{#if showExtraInfo}}

View File

@ -0,0 +1,8 @@
{{#if view.showBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
{{else}}
{{#if topic.unseen}}
<span class="badge-notification new-topic"></span>
{{/if}}
{{raw "list/posts-count-column" topic=topic tagName="div"}}
{{/if}}

View File

@ -1,5 +1,5 @@
<td class='posters'>
{{#each poster in posters}}
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster usernamePath="user.username" imageSize="small"}}</a>
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
{{/each}}
</td>

View File

@ -5,13 +5,9 @@
{{#each t in topics}}
<tr {{bind-attr class="t.archived"}}>
<td>
<div class='main-link clearfix'>
<div class='main-link'>
{{topic-status topic=t}}
{{topic-link t}}
{{topic-post-badges unread=t.unread
newPosts=t.new_posts
unseen=t.unseen
url=t.lastUnreadUrl}}
{{#if t.hasExcerpt}}
<div class="topic-excerpt">
@ -25,10 +21,14 @@
</div>
{{/if}}
</div>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}}
</div>
<div class='clearfix'></div>
<div class="topic-item-stats clearfix">
<div class="pull-right">
{{raw "list/posts-count-column" topic=t tagName="div"}}
{{raw "list/activity-column" topic=t tagName="div" class="num activity last"}}
<a href="{{t.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date t.bumped_at}}}'>{{t.last_poster_username}}</a>
</div>
{{#unless controller.hideCategory}}
<div class='category'>

View File

@ -1,12 +1,13 @@
<td>
<div class='main-link clearfix'>
<div class='main-link'>
{{raw "topic-status" topic=content}}
{{topic-link content}}
{{#if controller.showTopicPostBadges}}
{{raw "topic-post-badges" unread=content.unread newPosts=content.displayNewPosts unseen=content.unseen url=content.lastUnreadUrl}}
{{/if}}
{{raw "list/topic-excerpt" topic=content}}
</div>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}}
</div>
<div class="clearfix"></div>
<div class="topic-item-stats clearfix">
{{#unless controller.hideCategory}}
@ -17,10 +18,9 @@
{{plugin-outlet "topic-list-tags"}}
<div class="pull-right">
{{raw "list/posts-count-column" topic=content tagName="div"}}
<div class='num activity last'>
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
{{raw "list/activity-column" topic=content tagName="span" class="age"}}
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
</div>
</div>
<div class="clearfix"></div>

View File

@ -2,32 +2,27 @@
<div>
<div>
<input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}>
<label class="radio" for="system-avatar">{{bound-avatar controller "large" system_avatar_upload_id}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
<label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
</div>
<div>
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}>
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
</div>
{{#if allowImageUpload}}
<div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
<label class="radio" for="uploaded_avatar">
{{#if hasUploadedAvatar}}
{{#if uploadedAvatarTemplate}}
{{bound-avatar-template uploadedAvatarTemplate "large"}}
{{else}}
{{bound-avatar controller "large" custom_avatar_upload_id}}
{{/if}}
{{#if custom_avatar_template}}
{{bound-avatar-template custom_avatar_template "large"}}
{{i18n 'user.change_avatar.uploaded_avatar'}}
{{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}
</label>
{{avatar-uploader username=username
user_id=id
uploadedAvatarTemplate=uploadedAvatarTemplate
custom_avatar_upload_id=custom_avatar_upload_id
{{avatar-uploader user_id=id
uploadedAvatarTemplate=custom_avatar_template
uploadedAvatarId=custom_avatar_upload_id
uploading=uploading
done="useUploadedAvatar"}}
</div>
@ -36,6 +31,6 @@
</div>
<div class="modal-footer">
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=saveDisabled label="save"}}
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
</div>

View File

@ -1,4 +1,4 @@
<form {{action "saveAutoClose" on="submit"}}>
<form>
<div class="modal-body">
{{auto-close-form autoCloseTime=model.auto_close_time
autoCloseValid=auto_close_valid
@ -6,8 +6,8 @@
limited=model.details.auto_close_based_on_last_post }}
</div>
<div class="modal-footer">
<button class='btn btn-primary' type='submit' {{bind-attr disabled="auto_close_invalid"}}>{{i18n 'topic.auto_close_save'}}</button>
{{d-button class="btn-primary" disabled=auto_close_invalid label="topic.auto_close_save" action="saveAutoClose"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
<button class='btn pull-right' {{action "removeAutoClose"}}>{{i18n 'topic.auto_close_remove'}}</button>
{{d-button class="pull-right" action="removeAutoClose" label="topic.auto_close_remove"}}
</div>
</form>

View File

@ -1,36 +1,33 @@
<div>
<div class="modal-body reorder-categories">
<div id="rc-scroll-anchor"></div>
<table>
<thead>
<th class="th-pos">Position</th>
<th class="th-cat">Category</th>
</thead>
{{#each categoriesGrouped as |group|}}
<tbody>
{{#each group.cats as |cat|}}
<tr data-category-id="{{cat.id}}">
<td>
{{number-field number=cat.position}}
{{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}}
{{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}}
{{#if cat.hasBufferedChanges}}
{{d-button class="no-text" action="commit" icon="check"}}
{{/if}}
</td>
<td>{{category-badge cat allowUncategorized="true"}}</td>
</tr>
{{/each}}
</tbody>
{{/each}}
</table>
<div id="rc-scroll-bottom"></div>
</div>
<div class="modal-footer">
{{#if showApplyAll}}
{{d-button action="commit" icon="check" label="categories.reorder.apply_all"}}
{{/if}}
{{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
</div>
<div class="modal-body reorder-categories full-height-modal">
<div id="rc-scroll-anchor"></div>
<table>
<thead>
<th class="th-pos">{{i18n "categories.reorder.position"}}</th>
<th class="th-cat">{{i18n "categories.category"}}</th>
</thead>
{{#each categoriesOrdered as |cat|}}
<tr data-category-id="{{cat.id}}">
<td>
{{number-field number=cat.position}}
{{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}}
{{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}}
{{#if cat.hasBufferedChanges}}
{{d-button class="no-text" action="commit" icon="check"}}
{{/if}}
</td>
<td>{{category-badge cat allowUncategorized="true"}}</td>
</tr>
{{/each}}
</table>
<div id="rc-scroll-bottom"></div>
</div>
<div class="modal-footer">
{{#if showFixIndices}}
{{d-button action="fixIndices" icon="random" label="categories.reorder.fix_order" title="categories.reorder.fix_order_tooltip"}}
{{/if}}
{{#if showApplyAll}}
{{d-button action="commit" icon="check" label="categories.reorder.apply_all"}}
{{/if}}
{{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
</div>

View File

@ -3,10 +3,7 @@
{{navigation-bar navItems=navItems filterMode=filterMode}}
{{#if canCreateCategory}}
{{d-button action="createCategory" icon="plus" label="category.create"}}
{{#if siteSettings.fixed_category_positions}}
{{d-button action="reorderCategories" icon="random" label="category.reorder"}}
{{/if}}
{{categories-admin-dropdown}}
{{/if}}
{{#if canCreateTopic}}
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>

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