Merge remote-tracking branch 'upstream/master' into update-locale-vietnamese

* upstream/master: (185 commits)
  SECURITY: Upgrade rails.
  FIX: new user summary page was broken
  Version bump to v1.5.0.beta9
  Remove addressable from Discourse.
  UX: change glyph when inviting existing user to a topic
  FIX: Allow for large free disk space
  Revert "FIX: disk_space should be a BigDecimal to handle large disk (closes #3923)"
  UX: improve styling of messages and mobile view of messages
  FIX: correct counts on user summary
  FIX: link to filtered down list of badges from summary FEATURE: pick featured badges in summary page
  FIX: do not allow new email to be duplicate FIX: return proper error message when email already exists
  retain unactivated accounts a bit longer default
  FEATURE: blocked users can send and reply to private messages from staff
  Remove Arel patch that has been merged upstream.
  correct path
  little typo
  FIX: Missing tag in CSS.
  PERF: remove 10-20ms of work from every page view
  FIX: remove green background for wiki (this can be re-added via a customization if needed)
  Hotfix for unsubscribe via email
  ...

# Conflicts:
#	.tx/config
This commit is contained in:
Khoa, Le Ngoc 2016-01-26 12:44:29 +07:00
commit 06e637fc4a
438 changed files with 16120 additions and 10710 deletions

View File

@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, vi_VN: vi lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.clientenyml] [discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml file_filter = config/locales/client.<lang>.yml

View File

@ -46,11 +46,11 @@ gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox' gem 'onebox'
gem 'ember-rails' gem 'ember-rails'
gem 'ember-source', '1.12.1' gem 'ember-source', '1.12.2'
gem 'barber' gem 'barber'
gem 'babel-transpiler' gem 'babel-transpiler'
gem 'message_bus' gem 'message_bus', '2.0.0.beta.2'
gem 'rails_multisite' gem 'rails_multisite'
@ -83,7 +83,9 @@ gem 'omniauth-twitter'
gem 'omniauth-github-discourse', require: 'omniauth-github' gem 'omniauth-github-discourse', require: 'omniauth-github'
gem 'omniauth-oauth2', require: false gem 'omniauth-oauth2', require: false
gem 'omniauth-google-oauth2'
# this removes the dependency on 'addressable'
gem 'omniauth-google-oauth2', git: 'git://github.com/zquestz/omniauth-google-oauth2.git', ref: 'b492c4bb8286d35'
gem 'oj' gem 'oj'
gem 'pg' gem 'pg'
gem 'pry-rails', require: false gem 'pry-rails', require: false

View File

@ -1,54 +1,65 @@
GIT
remote: git://github.com/zquestz/omniauth-google-oauth2.git
revision: b492c4bb8286d35ae1168d7f2e5c57769bfe45a0
ref: b492c4bb8286d35
specs:
omniauth-google-oauth2 (0.3.0)
jwt (~> 1.0)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionmailer (4.2.5) actionmailer (4.2.5.1)
actionpack (= 4.2.5) actionpack (= 4.2.5.1)
actionview (= 4.2.5) actionview (= 4.2.5.1)
activejob (= 4.2.5) activejob (= 4.2.5.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.5) actionpack (4.2.5.1)
actionview (= 4.2.5) actionview (= 4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.5) actionview (4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
active_model_serializers (0.8.3) active_model_serializers (0.8.3)
activemodel (>= 3.0) activemodel (>= 3.0)
activejob (4.2.5) activejob (4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.5) activemodel (4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.5) activerecord (4.2.5.1)
activemodel (= 4.2.5) activemodel (= 4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
arel (~> 6.0) arel (~> 6.0)
activesupport (4.2.5) activesupport (4.2.5.1)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
annotate (2.6.10) annotate (2.7.0)
activerecord (>= 3.2, <= 4.3) activerecord (>= 3.2, < 6.0)
rake (~> 10.4) rake (~> 10.4)
arel (6.0.3) arel (6.0.3)
aws-sdk (2.1.29) aws-sdk (2.2.9)
aws-sdk-resources (= 2.1.29) aws-sdk-resources (= 2.2.9)
aws-sdk-core (2.1.29) aws-sdk-core (2.2.9)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.1.29) aws-sdk-resources (2.2.9)
aws-sdk-core (= 2.1.29) aws-sdk-core (= 2.2.9)
babel-source (5.8.19) babel-source (5.8.34)
babel-transpiler (0.7.0) babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6) babel-source (>= 4.0, < 6)
execjs (~> 2.0) execjs (~> 2.0)
@ -62,7 +73,7 @@ GEM
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
builder (3.2.2) builder (3.2.2)
byebug (6.0.2) byebug (8.2.1)
certified (1.0.0) certified (1.0.0)
coderay (1.1.0) coderay (1.1.0)
concurrent-ruby (1.0.0) concurrent-ruby (1.0.0)
@ -89,15 +100,15 @@ GEM
ember-source (>= 1.1.0) ember-source (>= 1.1.0)
jquery-rails (>= 1.0.17) jquery-rails (>= 1.0.17)
railties (>= 3.1) railties (>= 3.1)
ember-source (1.12.1) ember-source (1.12.2)
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.45.4) excon (0.45.4)
execjs (2.6.0) execjs (2.6.0)
exifr (1.2.3.1) exifr (1.2.4)
fabrication (2.9.8) fabrication (2.9.8)
fakeweb (1.3.0) fakeweb (1.3.0)
faraday (0.9.1) faraday (0.9.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
fast_stack (0.1.0) fast_stack (0.1.0)
@ -115,15 +126,15 @@ GEM
thor (~> 0.19.1) thor (~> 0.19.1)
fspath (2.1.1) fspath (2.1.1)
gctools (0.2.3) gctools (0.2.3)
given_core (3.5.4) given_core (3.7.1)
sorcerer (>= 0.3.7) sorcerer (>= 0.3.7)
globalid (0.3.6) globalid (0.3.6)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
guess_html_encoding (0.0.11) guess_html_encoding (0.0.11)
hashie (3.4.2) hashie (3.4.3)
highline (1.7.7) highline (1.7.8)
hike (1.2.3) hike (1.2.3)
hiredis (0.6.0) hiredis (0.6.1)
htmlentities (4.3.4) htmlentities (4.3.4)
http-cookie (1.0.2) http-cookie (1.0.2)
domain_name (~> 0.5) domain_name (~> 0.5)
@ -142,12 +153,12 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (1.8.3) json (1.8.3)
jwt (1.5.1) jwt (1.5.2)
kgio (2.10.0) kgio (2.10.0)
librarian (0.1.2) librarian (0.1.2)
highline highline
thor (~> 0.15) thor (~> 0.15)
libv8 (3.16.14.11) libv8 (3.16.14.13)
listen (0.7.3) listen (0.7.3)
logster (1.0.1) logster (1.0.1)
loofah (2.0.3) loofah (2.0.3)
@ -155,28 +166,28 @@ GEM
lru_redux (1.1.0) lru_redux (1.1.0)
mail (2.6.3) mail (2.6.3)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
memory_profiler (0.9.4) memory_profiler (0.9.6)
message_bus (1.1.1) message_bus (2.0.0.beta.2)
rack (>= 1.1.3) rack (>= 1.1.3)
redis redis
metaclass (0.0.4) metaclass (0.0.4)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99) mime-types (2.99)
mini_portile2 (2.0.0) mini_portile2 (2.0.0)
minitest (5.8.3) minitest (5.8.4)
mocha (1.1.0) mocha (1.1.0)
metaclass (~> 0.0.1) metaclass (~> 0.0.1)
mock_redis (0.15.2) mock_redis (0.15.4)
moneta (0.8.0) moneta (0.8.0)
msgpack (0.6.2) msgpack (0.7.4)
multi_json (1.11.2) multi_json (1.11.2)
multi_xml (0.5.5) multi_xml (0.5.5)
multipart-post (2.0.0) multipart-post (2.0.0)
mustache (1.0.2) mustache (1.0.2)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.6.7.1) nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2) mini_portile2 (~> 2.0.0.rc2)
nokogumbo (1.4.1) nokogumbo (1.4.7)
nokogiri nokogiri
oauth (0.4.7) oauth (0.4.7)
oauth2 (1.0.0) oauth2 (1.0.0)
@ -185,18 +196,15 @@ GEM
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (~> 1.2) rack (~> 1.2)
oj (2.12.14) oj (2.14.3)
omniauth (1.2.2) omniauth (1.3.1)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (~> 1.0) rack (>= 1.0, < 3)
omniauth-facebook (2.0.1) omniauth-facebook (3.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github-discourse (1.1.2) omniauth-github-discourse (1.1.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-google-oauth2 (0.2.5)
omniauth (> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth (1.1.0) omniauth-oauth (1.1.0)
oauth oauth
omniauth (~> 1.0) omniauth (~> 1.0)
@ -209,7 +217,7 @@ GEM
omniauth-twitter (1.2.1) omniauth-twitter (1.2.1)
json (~> 1.3) json (~> 1.3)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
onebox (1.5.31) onebox (1.5.33)
moneta (~> 0.8) moneta (~> 0.8)
multi_json (~> 1.11) multi_json (~> 1.11)
mustache mustache
@ -217,9 +225,9 @@ GEM
openid-redis-store (0.0.2) openid-redis-store (0.0.2)
redis redis
ruby-openid ruby-openid
pg (0.18.3) pg (0.18.4)
progress (3.1.0) progress (3.1.1)
pry (0.10.1) pry (0.10.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
@ -227,10 +235,10 @@ GEM
pry (>= 0.9.10, < 0.11.0) pry (>= 0.9.10, < 0.11.0)
pry-rails (0.3.4) pry-rails (0.3.4)
pry (>= 0.9.10) pry (>= 0.9.10)
puma (2.14.0) puma (2.15.3)
r2 (0.2.5) r2 (0.2.6)
rack (1.6.4) rack (1.6.4)
rack-mini-profiler (0.9.7) rack-mini-profiler (0.9.8)
rack (>= 1.1.3) rack (>= 1.1.3)
rack-openid (1.3.1) rack-openid (1.3.1)
rack (>= 1.1.0) rack (>= 1.1.0)
@ -239,16 +247,16 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.5) rails (4.2.5.1)
actionmailer (= 4.2.5) actionmailer (= 4.2.5.1)
actionpack (= 4.2.5) actionpack (= 4.2.5.1)
actionview (= 4.2.5) actionview (= 4.2.5.1)
activejob (= 4.2.5) activejob (= 4.2.5.1)
activemodel (= 4.2.5) activemodel (= 4.2.5.1)
activerecord (= 4.2.5) activerecord (= 4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.5) railties (= 4.2.5.1)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
@ -256,21 +264,21 @@ GEM
activesupport (>= 4.2.0.beta, < 5.0) activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.2) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
rails-observers (0.1.2) rails-observers (0.1.2)
activemodel (~> 4.0) activemodel (~> 4.0)
rails_multisite (1.0.3) rails_multisite (1.0.3)
railties (4.2.5) railties (4.2.5.1)
actionpack (= 4.2.5) actionpack (= 4.2.5.1)
activesupport (= 4.2.5) activesupport (= 4.2.5.1)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
raindrops (0.15.0) raindrops (0.15.0)
rake (10.4.2) rake (10.5.0)
rake-compiler (0.9.5) rake-compiler (0.9.5)
rake rake
rb-fsevent (0.9.6) rb-fsevent (0.9.7)
rb-inotify (0.9.5) rb-inotify (0.9.5)
ffi (>= 0.5.0) ffi (>= 0.5.0)
rbtrace (0.4.7) rbtrace (0.4.7)
@ -296,16 +304,16 @@ GEM
rspec-expectations (3.2.1) rspec-expectations (3.2.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0) rspec-support (~> 3.2.0)
rspec-given (3.5.4) rspec-given (3.7.1)
given_core (= 3.5.4) given_core (= 3.7.1)
rspec (>= 2.12) rspec (>= 2.14.0)
rspec-html-matchers (0.7.0) rspec-html-matchers (0.7.0)
nokogiri (~> 1) nokogiri (~> 1)
rspec (~> 3) rspec (~> 3)
rspec-mocks (3.2.1) rspec-mocks (3.2.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0) rspec-support (~> 3.2.0)
rspec-rails (3.2.1) rspec-rails (3.2.3)
actionpack (>= 3.0, < 4.3) actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3) railties (>= 3.0, < 4.3)
@ -319,10 +327,10 @@ GEM
ruby-readability (0.7.0) ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4) guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0) nokogiri (>= 1.6.0)
sanitize (4.0.0) sanitize (4.0.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (= 1.4.1) nokogumbo (~> 1.4.1)
sass (3.2.19) sass (3.2.19)
sass-rails (4.0.5) sass-rails (4.0.5)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
@ -336,17 +344,16 @@ GEM
shoulda-context (~> 1.0, >= 1.0.1) shoulda-context (~> 1.0, >= 1.0.1)
shoulda-matchers (>= 1.4.1, < 3.0) shoulda-matchers (>= 1.4.1, < 3.0)
shoulda-context (1.2.1) shoulda-context (1.2.1)
shoulda-matchers (2.7.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.0.1) sidekiq (4.0.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
json (~> 1.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sidekiq-statistic (1.2.0) sidekiq-statistic (1.2.0)
sidekiq (>= 3.3.4, < 5) sidekiq (>= 3.3.4, < 5)
simple-rss (1.3.1) simple-rss (1.3.1)
simplecov (0.10.0) simplecov (0.11.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (~> 1.8) json (~> 1.8)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
@ -382,7 +389,7 @@ GEM
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (1.4.1) tilt (1.4.1)
timecop (0.8.0) timecop (0.8.0)
trollop (2.1.1) trollop (2.1.2)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (2.7.2) uglifier (2.7.2)
@ -390,8 +397,8 @@ GEM
json (>= 1.8.0) json (>= 1.8.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.6) unf_ext (0.0.7.1)
unicorn (4.9.0) unicorn (5.0.1)
kgio (~> 2.6) kgio (~> 2.6)
rack rack
raindrops (~> 0.7) raindrops (~> 0.7)
@ -412,7 +419,7 @@ DEPENDENCIES
discourse-qunit-rails discourse-qunit-rails
discourse_email_parser discourse_email_parser
ember-rails ember-rails
ember-source (= 1.12.1) ember-source (= 1.12.2)
excon excon
fabrication (= 2.9.8) fabrication (= 2.9.8)
fakeweb (~> 1.3.0) fakeweb (~> 1.3.0)
@ -433,7 +440,7 @@ DEPENDENCIES
lru_redux lru_redux
mail mail
memory_profiler memory_profiler
message_bus message_bus (= 2.0.0.beta.2)
mime-types mime-types
minitest minitest
mocha mocha
@ -445,7 +452,7 @@ DEPENDENCIES
omniauth omniauth
omniauth-facebook omniauth-facebook
omniauth-github-discourse omniauth-github-discourse
omniauth-google-oauth2 omniauth-google-oauth2!
omniauth-oauth2 omniauth-oauth2
omniauth-openid omniauth-openid
omniauth-twitter omniauth-twitter

View File

@ -56,7 +56,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing ## Contributing
[![Build Status](https://travis-ci.org/discourse/discourse.svg)](https://travis-ci.org/discourse/discourse) [![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse) [![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse)
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that

View File

@ -1,3 +0,0 @@
import AdminEmailSkippedController from "admin/controllers/admin-email-skipped";
export default AdminEmailSkippedController.extend();

View File

@ -0,0 +1,11 @@
import IncomingEmail from 'admin/models/incoming-email';
export default Ember.Controller.extend({
loadMore() {
return IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
.then(incoming => {
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(incoming);
});
}
});

View File

@ -0,0 +1,11 @@
import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({
loadMore() {
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
.then(logs => {
if (logs.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(logs);
});
}
});

View File

@ -0,0 +1,9 @@
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
import debounce from 'discourse/lib/debounce';
import IncomingEmail from 'admin/models/incoming-email';
export default AdminEmailIncomingsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
}, 250).observes("filter.{from,to,subject}")
});

View File

@ -0,0 +1,9 @@
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
import debounce from 'discourse/lib/debounce';
import IncomingEmail from 'admin/models/incoming-email';
export default AdminEmailIncomingsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
}, 250).observes("filter.{from,to,subject,error}")
});

View File

@ -1,12 +1,9 @@
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
import debounce from 'discourse/lib/debounce'; import debounce from 'discourse/lib/debounce';
import EmailLog from 'admin/models/email-log'; import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({ export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() { filterEmailLogs: debounce(function() {
var self = this; EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
EmailLog.findAll(this.get("filter")).then(function(logs) { }, 250).observes("filter.{user,address,type,reply_key}")
self.set("model", logs);
});
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.reply_key")
}); });

View File

@ -1,8 +1,9 @@
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
import debounce from 'discourse/lib/debounce'; import debounce from 'discourse/lib/debounce';
import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({ export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() { filterEmailLogs: debounce(function() {
const EmailLog = require('admin/models/email-log').default;
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs)); EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.skipped_reason") }, 250).observes("filter.{user,address,type,skipped_reason}")
}); });

View File

@ -1,5 +1,6 @@
import { exportEntity } from 'discourse/lib/export-csv'; import { exportEntity } from 'discourse/lib/export-csv';
import { outputExportResult } from 'discourse/lib/export-result'; import { outputExportResult } from 'discourse/lib/export-result';
import Report from 'admin/models/report';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
viewMode: 'table', viewMode: 'table',
@ -20,9 +21,9 @@ export default Ember.Controller.extend({
var q; var q;
this.set("refreshing", true); this.set("refreshing", true);
if (this.get('categoryId') === "all") { if (this.get('categoryId') === "all") {
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate")); q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"));
} else { } else {
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId")); q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"));
} }
q.then(m => this.set("model", m)).finally(() => this.set("refreshing", false)); q.then(m => this.set("model", m)).finally(() => this.set("refreshing", false));
}, },

View File

@ -2,15 +2,13 @@ export default Ember.Controller.extend({
needs: ['modal'], needs: ['modal'],
modelChanged: function(){ modelChanged: function(){
const model = this.get('model');
var grouping = Em.Object.extend({}); const copy = Em.A();
const store = this.store;
var model = this.get('model');
var copy = Em.A();
if(model){ if(model){
model.forEach(function(o){ model.forEach(function(o){
copy.pushObject(grouping.create(o)); copy.pushObject(store.createRecord('badge-grouping', o));
}); });
} }
@ -18,8 +16,8 @@ export default Ember.Controller.extend({
}.observes('model'), }.observes('model'),
moveItem: function(item, delta){ moveItem: function(item, delta){
var copy = this.get('workingCopy'); const copy = this.get('workingCopy');
var index = copy.indexOf(item); const index = copy.indexOf(item);
if (index + delta < 0 || index + delta >= copy.length){ if (index + delta < 0 || index + delta >= copy.length){
return; return;
} }
@ -50,14 +48,14 @@ export default Ember.Controller.extend({
item.set("editing", false); item.set("editing", false);
}, },
add: function(){ add: function(){
var obj = Em.Object.create({editing: true, name: "Enter Name"}); const obj = this.store.createRecord('badge-grouping', {editing: true, name: I18n.t('admin.badges.badge_grouping')});
this.get('workingCopy').pushObject(obj); this.get('workingCopy').pushObject(obj);
}, },
saveAll: function(){ saveAll: function(){
var self = this; const self = this;
var items = this.get('workingCopy'); var items = this.get('workingCopy');
var groupIds = items.map(function(i){return i.get("id") || -1;}); const groupIds = items.map(function(i){return i.get("id") || -1;});
var names = items.map(function(i){return i.get("name");}); const names = items.map(function(i){return i.get("name");});
Discourse.ajax('/admin/badges/badge_groupings',{ Discourse.ajax('/admin/badges/badge_groupings',{
data: {ids: groupIds, names: names}, data: {ids: groupIds, names: names},
@ -66,14 +64,13 @@ export default Ember.Controller.extend({
items = self.get("model"); items = self.get("model");
items.clear(); items.clear();
data.badge_groupings.forEach(function(g){ data.badge_groupings.forEach(function(g){
items.pushObject(Em.Object.create(g)); items.pushObject(self.store.createRecord('badge-grouping', g));
}); });
self.set('model', null); self.set('model', null);
self.set('workingCopy', null); self.set('workingCopy', null);
self.send('closeModal'); self.send('closeModal');
},function(){ },function(){
// TODO we can do better bootbox.alert(I18n.t('generic_error'));
bootbox.alert("Something went wrong");
}); });
} }
} }

View File

@ -9,6 +9,8 @@ const AdminUser = Discourse.User.extend({
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Group.create(g)), customGroups: Em.computed.filter("groups", (g) => !g.automatic && Group.create(g)),
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Group.create(g)), automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Group.create(g)),
canViewProfile: Ember.computed.or("active", "staged"),
generateApiKey() { generateApiKey() {
const self = this; const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", { return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
@ -264,6 +266,7 @@ const AdminUser = Discourse.User.extend({
}, },
unblock() { unblock() {
this.set('blockingUser', true);
return Discourse.ajax('/admin/users/' + this.id + '/unblock', { return Discourse.ajax('/admin/users/' + this.id + '/unblock', {
type: 'PUT' type: 'PUT'
}).then(function() { }).then(function() {
@ -275,14 +278,33 @@ const AdminUser = Discourse.User.extend({
}, },
block() { block() {
return Discourse.ajax('/admin/users/' + this.id + '/block', { const user = this,
message = I18n.t("admin.user.block_confirm");
const performBlock = function() {
user.set('blockingUser', true);
return Discourse.ajax('/admin/users/' + user.id + '/block', {
type: 'PUT' type: 'PUT'
}).then(function() { }).then(function() {
window.location.reload(); window.location.reload();
}).catch(function(e) { }).catch(function(e) {
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body }); var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error); bootbox.alert(error);
user.set('blockingUser', false);
}); });
};
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.block_accept'),
"class": "btn btn-danger",
"callback": function() { performBlock(); }
}];
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
}, },
sendActivationEmail() { sendActivationEmail() {

View File

@ -4,7 +4,7 @@ const EmailLog = Discourse.Model.extend({});
EmailLog.reopenClass({ EmailLog.reopenClass({
create: function(attrs) { create(attrs) {
attrs = attrs || {}; attrs = attrs || {};
if (attrs.user) { if (attrs.user) {
@ -14,16 +14,15 @@ EmailLog.reopenClass({
return this._super(attrs); return this._super(attrs);
}, },
findAll: function(filter) { findAll(filter, offset) {
filter = filter || {}; filter = filter || {};
var status = filter.status || "all"; offset = offset || 0;
const status = filter.status || "sent";
filter = _.omit(filter, "status"); filter = _.omit(filter, "status");
return Discourse.ajax("/admin/email/" + status + ".json", { data: filter }).then(function(logs) { return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
return _.map(logs, function (log) { .then(logs => _.map(logs, log => EmailLog.create(log)));
return EmailLog.create(log);
});
});
} }
}); });

View File

@ -0,0 +1,29 @@
import AdminUser from 'admin/models/admin-user';
const IncomingEmail = Discourse.Model.extend({});
IncomingEmail.reopenClass({
create(attrs) {
attrs = attrs || {};
if (attrs.user) {
attrs.user = AdminUser.create(attrs.user);
}
return this._super(attrs);
},
findAll(filter, offset) {
filter = filter || {};
offset = offset || 0;
const status = filter.status || "received";
filter = _.omit(filter, "status");
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
.then(incomings => _.map(incomings, incoming => IncomingEmail.create(incoming)));
}
});
export default IncomingEmail;

View File

@ -1,4 +1,5 @@
import Badge from 'discourse/models/badge'; import Badge from 'discourse/models/badge';
import BadgeGrouping from 'discourse/models/badge-grouping';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
_json: null, _json: null,
@ -13,14 +14,19 @@ export default Discourse.Route.extend({
setupController: function(controller, model) { setupController: function(controller, model) {
var json = this._json, var json = this._json,
triggers = []; triggers = [],
badgeGroupings = [];
_.each(json.admin_badges.triggers,function(v,k){ _.each(json.admin_badges.triggers,function(v,k){
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)}); triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
}); });
json.badge_groupings.forEach(function(badgeGroupingJson) {
badgeGroupings.push(BadgeGrouping.create(badgeGroupingJson));
});
controller.setProperties({ controller.setProperties({
badgeGroupings: json.badge_groupings, badgeGroupings: badgeGroupings,
badgeTypes: json.badge_types, badgeTypes: json.badge_types,
protectedSystemFields: json.admin_badges.protected_system_fields, protectedSystemFields: json.admin_badges.protected_system_fields,
badgeTriggers: triggers, badgeTriggers: triggers,

View File

@ -1,2 +0,0 @@
import AdminEmailLogs from 'admin/routes/admin-email-logs';
export default AdminEmailLogs.extend({ status: "all" });

View File

@ -0,0 +1,14 @@
import IncomingEmail from 'admin/models/incoming-email';
export default Discourse.Route.extend({
model() {
return IncomingEmail.findAll({ status: this.get("status") });
},
setupController(controller, model) {
controller.set("model", model);
controller.set("filter", { status: this.get("status") });
}
});

View File

@ -1,11 +1,11 @@
import EmailSettings from 'admin/models/email-settings'; import EmailSettings from 'admin/models/email-settings';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
model: function() { model() {
return EmailSettings.find(); return EmailSettings.find();
}, },
renderTemplate: function() { renderTemplate() {
this.render('admin/templates/email_index', { into: 'adminEmail' }); this.render('admin/templates/email_index', { into: 'adminEmail' });
} }
}); });

View File

@ -1,27 +1,14 @@
import EmailLog from 'admin/models/email-log'; import EmailLog from 'admin/models/email-log';
/**
Handles routes related to viewing email logs.
@class AdminEmailSentRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
export default Discourse.Route.extend({ export default Discourse.Route.extend({
model: function() { model() {
return EmailLog.findAll({ status: this.get("status") }); return EmailLog.findAll({ status: this.get("status") });
}, },
setupController: function(controller, model) { setupController(controller, model) {
controller.set("model", model); controller.set("model", model);
// resets the filters
controller.set("filter", { status: this.get("status") }); controller.set("filter", { status: this.get("status") });
},
renderTemplate: function() {
this.render("admin/templates/email_" + this.get("status"), { into: "adminEmail" });
} }
}); });

View File

@ -0,0 +1,2 @@
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
export default AdminEmailIncomings.extend({ status: "received" });

View File

@ -0,0 +1,2 @@
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
export default AdminEmailIncomings.extend({ status: "rejected" });

View File

@ -8,9 +8,10 @@ export default {
}); });
this.resource('adminEmail', { path: '/email'}, function() { this.resource('adminEmail', { path: '/email'}, function() {
this.route('all');
this.route('sent'); this.route('sent');
this.route('skipped'); this.route('skipped');
this.route('received');
this.route('rejected');
this.route('previewDigest', { path: '/preview-digest' }); this.route('previewDigest', { path: '/preview-digest' });
}); });

View File

@ -2,25 +2,22 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div> <div>
<label for="name">{{i18n 'admin.badges.name'}}</label> <label for="name">{{i18n 'admin.badges.name'}}</label>
{{#if readOnly}}
{{input type="text" name="name" value=buffered.displayName disabled=true}}
{{else}}
{{input type="text" name="name" value=buffered.name}} {{input type="text" name="name" value=buffered.name}}
</div>
{{#if showDisplayName}}
<div>
<strong>{{i18n 'admin.badges.display_name'}}</strong>
{{buffered.displayName}}
</div>
{{/if}} {{/if}}
</div>
<div> <div>
<label for="name">{{i18n 'admin.badges.icon'}}</label> <label for="icon">{{i18n 'admin.badges.icon'}}</label>
{{input type="text" name="name" value=buffered.icon}} {{input type="text" name="icon" value=buffered.icon}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p> <p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
</div> </div>
<div> <div>
<label for="name">{{i18n 'admin.badges.image'}}</label> <label for="image">{{i18n 'admin.badges.image'}}</label>
{{input type="text" name="name" value=buffered.image}} {{input type="text" name="image" value=buffered.image}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p> <p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
</div> </div>
@ -40,7 +37,7 @@
value=buffered.badge_grouping_id value=buffered.badge_grouping_id
content=badgeGroupings content=badgeGroupings
optionValuePath="content.id" optionValuePath="content.id"
optionLabelPath="content.name"}} optionLabelPath="content.displayName"}}
&nbsp;<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button> &nbsp;<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
</div> </div>

View File

@ -0,0 +1,55 @@
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
</tr>
{{#each email in model}}
<tr>
<td class="time">{{format-date email.created_at}}</td>
<td class="username">
<div>
{{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>
{{#if email.post_url}}
<a href="{{email.post_url}}">{{email.subject}}</a>
{{else}}
{{email.subject}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -0,0 +1,52 @@
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
<th>{{i18n 'admin.email.incoming_emails.error'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
</tr>
{{#each email in model}}
<tr>
<td class="time">{{format-date email.created_at}}</td>
<td class="username">
<div>
{{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>{{email.subject}}</td>
<td class="error">{{email.error}}</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,4 +1,4 @@
<table class='table'> <table class='table email-list'>
<thead> <thead>
<tr> <tr>
<th>{{i18n 'admin.email.sent_at'}}</th> <th>{{i18n 'admin.email.sent_at'}}</th>
@ -37,3 +37,5 @@
{{/each}} {{/each}}
</table> </table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,4 +1,4 @@
<table class='table'> <table class='table email-list'>
<thead> <thead>
<tr> <tr>
<th>{{i18n 'admin.email.time'}}</th> <th>{{i18n 'admin.email.time'}}</th>
@ -37,3 +37,5 @@
{{/each}} {{/each}}
</table> </table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,10 +1,11 @@
{{#admin-nav}} {{#admin-nav}}
{{nav-item route='adminEmail.index' label='admin.email.settings'}} {{nav-item route='adminEmail.index' label='admin.email.settings'}}
{{nav-item route='adminEmail.all' label='admin.email.all'}} {{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}}
{{nav-item route='adminEmail.sent' label='admin.email.sent'}} {{nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}} {{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}} {{nav-item route='adminEmail.received' label='admin.email.received'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} {{nav-item route='adminEmail.rejected' label='admin.email.rejected'}}
{{/admin-nav}} {{/admin-nav}}
<div class="admin-container"> <div class="admin-container">

View File

@ -1,39 +0,0 @@
<table class='table'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.user'}}</th>
<th>{{i18n 'admin.email.to_address'}}</th>
<th>{{i18n 'admin.email.email_type'}}</th>
<th>{{i18n 'admin.email.skipped_reason'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
</tr>
{{#each l in model}}
<tr>
<td>{{format-date l.created_at}}</td>
<td>
{{#if l.user}}
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>{{l.skipped_reason}}</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
</table>

View File

@ -5,15 +5,15 @@
<li> <li>
{{#if wc.editing}} {{#if wc.editing}}
{{input value=wc.name}} {{input value=wc.name}}
<button {{action "save" wc}}><i class="fa fa-check"></i></button> <button {{action "save" wc}} class="btn no-text">{{fa-icon 'check'}}</button>
{{else}} {{else}}
{{wc.name}} {{wc.displayName}}
{{/if}} {{/if}}
<div class='actions'> <div class='actions'>
<button {{action "edit" wc}}><i class="fa fa-pencil"></i></button> <button {{action "edit" wc}} class="btn no-text" {{bind-attr disabled="wc.system"}}>{{fa-icon 'pencil'}}</button>
<button {{action "up" wc}}><i class="fa fa-toggle-up"></i></button> <button {{action "up" wc}} class="btn no-text">{{fa-icon 'toggle-up'}}</button>
<button {{action "down" wc}}><i class="fa fa-toggle-down"></i></button> <button {{action "down" wc}} class="btn no-text">{{fa-icon 'toggle-down'}}</button>
<button {{action "delete" wc}}><i class="fa fa-times"></i></button> <button {{action "delete" wc}} class="btn no-text btn-danger" {{bind-attr disabled="wc.system"}}>{{fa-icon 'times'}}</button>
</div> </div>
</li> </li>
{{/each}} {{/each}}

View File

@ -1,11 +1,13 @@
<section class="details {{unless model.active 'not-activated'}}"> <section class="details {{unless model.active 'not-activated'}}">
<div class='user-controls'> <div class='user-controls'>
{{#if model.active}} {{#if model.canViewProfile}}
{{#link-to 'user' model class="btn"}} {{#link-to 'user' model class="btn"}}
{{fa-icon "user"}} {{fa-icon "user"}}
{{i18n 'admin.user.show_public_profile'}} {{i18n 'admin.user.show_public_profile'}}
{{/link-to}} {{/link-to}}
{{/if}}
{{#if model.active}}
{{#if model.can_impersonate}} {{#if model.can_impersonate}}
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}"> <button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
{{fa-icon "crosshairs"}} {{fa-icon "crosshairs"}}
@ -327,15 +329,29 @@
<div class='field'>{{i18n 'admin.user.blocked'}}</div> <div class='field'>{{i18n 'admin.user.blocked'}}</div>
<div class='value'>{{model.blocked}}</div> <div class='value'>{{model.blocked}}</div>
<div class='controls'> <div class='controls'>
{{#conditional-loading-spinner size="small" condition=model.blockingUser}}
{{#if model.blocked}} {{#if model.blocked}}
<button class='btn' {{action "unblock" target="content"}}> <button class='btn' {{action "unblock" target="content"}}>
{{fa-icon "thumbs-o-up"}} {{fa-icon "thumbs-o-up"}}
{{i18n 'admin.user.unblock'}} {{i18n 'admin.user.unblock'}}
</button> </button>
{{i18n 'admin.user.block_explanation'}} {{i18n 'admin.user.block_explanation'}}
{{else}}
<button class='btn' {{action "block" target="content"}}>
{{fa-icon "ban"}}
{{i18n 'admin.user.block'}}
</button>
{{i18n 'admin.user.block_explanation'}}
{{/if}} {{/if}}
{{/conditional-loading-spinner}}
</div> </div>
</div> </div>
<div class="display-row">
<div class='field'>{{i18n 'admin.user.staged'}}</div>
<div class='value'>{{model.staged}}</div>
<div class='controls'>{{i18n 'admin.user.stage_explanation'}}</div>
</div>
</section> </section>
<section class='details'> <section class='details'>

View File

@ -0,0 +1,14 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -0,0 +1,14 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -0,0 +1,5 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-received"
});

View File

@ -0,0 +1,5 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-rejected"
});

View File

@ -0,0 +1,5 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-sent"
});

View File

@ -0,0 +1,5 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-skipped"
});

View File

@ -6,9 +6,9 @@ import PermissionType from 'discourse/models/permission-type';
export default ComboboxView.extend({ export default ComboboxView.extend({
classNames: ['combobox category-combobox'], classNames: ['combobox category-combobox'],
overrideWidths: true,
dataAttributes: ['id', 'description_text'], dataAttributes: ['id', 'description_text'],
valueBinding: Ember.Binding.oneWay('source'), valueBinding: Ember.Binding.oneWay('source'),
overrideWidths: true,
castInteger: true, castInteger: true,
@computed("scopedCategoryId", "categories") @computed("scopedCategoryId", "categories")
@ -22,7 +22,6 @@ export default ComboboxView.extend({
return categories.filter(c => { return categories.filter(c => {
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; } if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory')) { return false; } if (c.get('isUncategorizedCategory')) { return false; }
if (c.get('contains_messages')) { return false; }
return c.get('permission') === PermissionType.FULL; return c.get('permission') === PermissionType.FULL;
}); });
}, },

View File

@ -1,18 +1,19 @@
import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
export default Ember.Component.extend({ export default Ember.Component.extend({
_initializeAutocomplete: function() { _initializeAutocomplete: function() {
const self = this, const self = this,
template = this.container.lookup('template:category-group-autocomplete.raw'), template = this.container.lookup('template:category-group-autocomplete.raw'),
regexp = new RegExp("href=['\"]" + Discourse.getURL('/c/') + "([^'\"]+)"); regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
this.$('input').autocomplete({ this.$('input').autocomplete({
items: this.get('categories'), items: this.get('categories'),
single: false, single: false,
allowAny: false, allowAny: false,
dataSource(term){ dataSource(term){
return Discourse.Category.list().filter(function(category){ return Category.list().filter(function(category){
const regex = new RegExp(term, "i"); const regex = new RegExp(term, "i");
return category.get("name").match(regex) && return category.get("name").match(regex) &&
!_.contains(self.get('blacklist') || [], category) && !_.contains(self.get('blacklist') || [], category) &&
@ -22,7 +23,7 @@ export default Ember.Component.extend({
onChangeItems(items) { onChangeItems(items) {
const categories = _.map(items, function(link) { const categories = _.map(items, function(link) {
const slug = link.match(regexp)[1]; const slug = link.match(regexp)[1];
return Discourse.Category.findSingleBySlug(slug); return Category.findSingleBySlug(slug);
}); });
Em.run.next(() => self.set("categories", categories)); Em.run.next(() => self.set("categories", categories));
}, },

View File

@ -1,6 +1,7 @@
import userSearch from 'discourse/lib/user-search'; import userSearch from 'discourse/lib/user-search';
import { default as computed, on } from 'ember-addons/ember-computed-decorators'; import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ['wmd-controls'], classNames: ['wmd-controls'],
@ -111,13 +112,19 @@ export default Ember.Component.extend({
$preview.scrollTop(desired + 50); $preview.scrollTop(desired + 50);
}, },
_renderUnseen: function($preview, unseen) { _renderUnseenMentions: function($preview, unseen) {
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => { fetchUnseenMentions($preview, unseen).then(() => {
linkSeenMentions($preview, this.siteSettings); linkSeenMentions($preview, this.siteSettings);
this._warnMentionedGroups($preview); this._warnMentionedGroups($preview);
}); });
}, },
_renderUnseenCategoryHashtags: function($preview, unseen) {
fetchUnseenCategoryHashtags(unseen).then(() => {
linkSeenCategoryHashtags($preview);
});
},
_warnMentionedGroups($preview) { _warnMentionedGroups($preview) {
Ember.run.scheduleOnce('afterRender', () => { Ember.run.scheduleOnce('afterRender', () => {
this._warnedMentions = this._warnedMentions || []; this._warnedMentions = this._warnedMentions || [];
@ -386,11 +393,17 @@ export default Ember.Component.extend({
// Paint mentions // Paint mentions
const unseen = linkSeenMentions($preview, this.siteSettings); const unseen = linkSeenMentions($preview, this.siteSettings);
if (unseen.length) { if (unseen.length) {
Ember.run.debounce(this, this._renderUnseen, $preview, unseen, 500); Ember.run.debounce(this, this._renderUnseenMentions, $preview, unseen, 500);
} }
this._warnMentionedGroups($preview); this._warnMentionedGroups($preview);
// Paint category hashtags
const unseenHashtags = linkSeenCategoryHashtags($preview);
if (unseenHashtags.length) {
Ember.run.debounce(this, this._renderUnseenCategoryHashtags, $preview, unseenHashtags, 500);
}
const post = this.get('composer.post'); const post = this.get('composer.post');
let refresh = false; let refresh = false;

View File

@ -2,6 +2,8 @@
import loadScript from 'discourse/lib/load-script'; import loadScript from 'discourse/lib/load-script';
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
import Category from 'discourse/models/category';
import { SEPARATOR as categoryHashtagSeparator } from 'discourse/lib/category-hashtags';
// Our head can be a static string or a function that returns a string // Our head can be a static string or a function that returns a string
// based on input (like for numbered lists). // based on input (like for numbered lists).
@ -41,7 +43,7 @@ function Toolbar() {
id: 'italic', id: 'italic',
group: 'fontStyles', group: 'fontStyles',
shortcut: 'I', shortcut: 'I',
perform: e => e.applySurround('*', '*', 'italic_text') perform: e => e.applySurround('_', '_', 'italic_text')
}); });
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'}); this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
@ -175,7 +177,11 @@ export default Ember.Component.extend({
@on('didInsertElement') @on('didInsertElement')
_startUp() { _startUp() {
this._applyEmojiAutocomplete(); const container = this.get('container'),
$editorInput = this.$('.d-editor-input');
this._applyEmojiAutocomplete(container, $editorInput);
this._applyCategoryHashtagAutocomplete(container, $editorInput);
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true)); loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
@ -243,14 +249,49 @@ export default Ember.Component.extend({
Ember.run.debounce(this, this._updatePreview, 30); Ember.run.debounce(this, this._updatePreview, 30);
}, },
_applyEmojiAutocomplete() { _applyCategoryHashtagAutocomplete(container, $editorInput) {
const template = container.lookup('template:category-group-autocomplete.raw');
$editorInput.autocomplete({
template: template,
key: '#',
transformComplete(category) {
return Category.slugFor(category, categoryHashtagSeparator);
},
dataSource(term) {
return Category.search(term);
},
triggerRule(textarea, opts) {
const result = Discourse.Utilities.caretRowCol(textarea);
const row = result.rowNum;
var col = result.colNum;
var line = textarea.value.split("\n")[row - 1];
if (opts && opts.backSpace) {
col = col - 1;
line = line.slice(0, line.length - 1);
// Don't trigger autocomplete when backspacing into a `#category |` => `#category|`
if (/^#{1}\w+/.test(line)) return false;
}
if (col < 6) {
// Don't trigger autocomplete when ATX-style headers are used
return (line.slice(0, col) !== "#".repeat(col));
} else {
return true;
}
}
});
},
_applyEmojiAutocomplete(container, $editorInput) {
if (!this.siteSettings.enable_emoji) { return; } if (!this.siteSettings.enable_emoji) { return; }
const container = this.container;
const template = container.lookup('template:emoji-selector-autocomplete.raw'); const template = container.lookup('template:emoji-selector-autocomplete.raw');
const self = this; const self = this;
this.$('.d-editor-input').autocomplete({ $editorInput.autocomplete({
template: template, template: template,
key: ":", key: ":",

View File

@ -3,22 +3,24 @@ import loadScript from "discourse/lib/load-script";
import { on } from "ember-addons/ember-computed-decorators"; import { on } from "ember-addons/ember-computed-decorators";
export default Em.Component.extend({ export default Em.Component.extend({
tagName: "input", classNames: ["date-picker-wrapper"],
classNames: ["date-picker"],
_picker: null, _picker: null,
@on("didInsertElement") @on("didInsertElement")
_loadDatePicker() { _loadDatePicker() {
const input = this.$()[0]; const input = this.$(".date-picker")[0];
loadScript("/javascripts/pikaday.js").then(() => { loadScript("/javascripts/pikaday.js").then(() => {
this._picker = new Pikaday({ let default_opts = {
field: input, field: input,
container: this.$()[0],
format: "YYYY-MM-DD", format: "YYYY-MM-DD",
defaultDate: moment().add(1, "day").toDate(), defaultDate: moment().add(1, "day").toDate(),
minDate: new Date(), minDate: new Date(),
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")), onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD"))
}); };
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
}); });
}, },
@ -27,4 +29,8 @@ export default Em.Component.extend({
this._picker = null; this._picker = null;
}, },
_opts() {
return null;
}
}); });

View File

@ -4,10 +4,15 @@ import { setting } from 'discourse/lib/computed';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ["title"], classNames: ["title"],
linkUrl: function() { targetUrl: function() {
return Discourse.getURL('/'); // For overriding by customizations
return '/';
}.property(), }.property(),
linkUrl: function() {
return Discourse.getURL(this.get('targetUrl'));
}.property('targetUrl'),
showSmallLogo: function() { showSmallLogo: function() {
return !Discourse.Mobile.mobileView && this.get("minimized"); return !Discourse.Mobile.mobileView && this.get("minimized");
}.property("minimized"), }.property("minimized"),
@ -27,7 +32,7 @@ export default Ember.Component.extend({
e.preventDefault(); e.preventDefault();
DiscourseURL.routeTo('/'); DiscourseURL.routeTo(this.get('targetUrl'));
return false; return false;
} }
}); });

View File

@ -29,7 +29,9 @@ export default Ember.Component.extend({
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase(); badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
} }
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug); var username = it.get('data.username');
username = username ? "?username=" + username.toLowerCase() : "";
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
} }
const topicId = it.get('topic_id'); const topicId = it.get('topic_id');

View File

@ -349,6 +349,21 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
this.sendAction('toggleBookmark', post); this.sendAction('toggleBookmark', post);
}, },
// Wiki button
buttonForWiki(post) {
if (!post.get('can_wiki')) return;
if (post.get('wiki')) {
return new Button('wiki', 'post.controls.unwiki', 'pencil-square-o', {className: 'wiki wikied'});
} else {
return new Button('wiki', 'post.controls.wiki', 'pencil-square-o', {className: 'wiki'});
}
},
clickWiki(post) {
this.sendAction('toggleWiki', post);
},
buttonForAdmin() { buttonForAdmin() {
if (!Discourse.User.currentProp('canManageTopic')) { return; } if (!Discourse.User.currentProp('canManageTopic')) { return; }
return new Button('admin', 'post.controls.admin', 'wrench'); return new Button('admin', 'post.controls.admin', 'wrench');
@ -357,10 +372,7 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
renderAdminPopup(post, buffer) { renderAdminPopup(post, buffer) {
if (!Discourse.User.currentProp('canManageTopic')) { return; } if (!Discourse.User.currentProp('canManageTopic')) { return; }
const isWiki = post.get('wiki'), const isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
wikiIcon = iconHTML('pencil-square-o'),
wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
postTypeIcon = iconHTML('shield'), postTypeIcon = iconHTML('shield'),
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'), postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
rebakePostIcon = iconHTML('cog'), rebakePostIcon = iconHTML('cog'),
@ -373,7 +385,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
const html = '<div class="post-admin-menu popup-menu">' + const html = '<div class="post-admin-menu popup-menu">' +
'<h3>' + I18n.t('admin_title') + '</h3>' + '<h3>' + I18n.t('admin_title') + '</h3>' +
'<ul>' + '<ul>' +
'<li class="btn" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') + (Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' + '<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') + (post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
@ -393,10 +404,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
}); });
}, },
clickToggleWiki() {
this.sendAction('toggleWiki', this.get('post'));
},
clickTogglePostType() { clickTogglePostType() {
this.sendAction("togglePostType", this.get("post")); this.sendAction("togglePostType", this.get("post"));
}, },

View File

@ -1,4 +1,5 @@
import { relativeAge } from 'discourse/lib/formatter'; import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
import computed from 'ember-addons/ember-computed-decorators';
const icons = { const icons = {
'closed.enabled': 'lock', 'closed.enabled': 'lock',
@ -13,16 +14,20 @@ const icons = {
'pinned_globally.disabled': 'thumb-tack unpinned', 'pinned_globally.disabled': 'thumb-tack unpinned',
'visible.enabled': 'eye', 'visible.enabled': 'eye',
'visible.disabled': 'eye-slash', 'visible.disabled': 'eye-slash',
'split_topic': 'sign-out' 'split_topic': 'sign-out',
'invited_user': 'plus-circle',
'removed_user': 'minus-circle'
}; };
export function actionDescription(actionCode, createdAt) { export function actionDescription(actionCode, createdAt, username) {
return function() { return function() {
const ac = this.get(actionCode); const ac = this.get(actionCode);
if (ac) { if (ac) {
const dt = new Date(this.get(createdAt)); const dt = new Date(this.get(createdAt));
const when = relativeAge(dt, {format: 'medium-with-ago'}); const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
return I18n.t(`action_codes.${ac}`, {when}).htmlSafe(); const u = this.get(username);
const who = u ? `<a class="mention" href="/users/${u}">@${u}</a>` : "";
return I18n.t(`action_codes.${ac}`, { who, when }).htmlSafe();
} }
}.property(actionCode, createdAt); }.property(actionCode, createdAt);
} }
@ -31,18 +36,19 @@ export default Ember.Component.extend({
layoutName: 'components/small-action', // needed because `time-gap` inherits from this layoutName: 'components/small-action', // needed because `time-gap` inherits from this
classNames: ['small-action'], classNames: ['small-action'],
description: actionDescription('actionCode', 'post.created_at'), description: actionDescription('actionCode', 'post.created_at', 'post.action_code_who'),
icon: function() { @computed("actionCode")
return icons[this.get('actionCode')] || 'exclamation'; icon(actionCode) {
}.property('actionCode'), return icons[actionCode] || 'exclamation';
},
actions: { actions: {
edit: function() { edit() {
this.sendAction('editPost', this.get('post')); this.sendAction('editPost', this.get('post'));
}, },
delete: function() { delete() {
this.sendAction('deletePost', this.get('post')); this.sendAction('deletePost', this.get('post'));
} }
} }

View File

@ -4,7 +4,7 @@ import { actionDescription } from "discourse/components/small-action";
export default Ember.Component.extend({ export default Ember.Component.extend({
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"], classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"), moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
actionDescription: actionDescription("item.action_code", "item.created_at"), actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
actions: { actions: {
removeBookmark(userAction) { removeBookmark(userAction) {

View File

@ -3,5 +3,13 @@ export default Ember.Component.extend({
showGrantCount: function() { showGrantCount: function() {
return this.get('count') && this.get('count') > 1; return this.get('count') && this.get('count') > 1;
}.property('count') }.property('count'),
badgeUrl: function(){
// NOTE: I tried using a link-to helper here but the queryParams mean it fails
var username = this.get('user.username_lower') || '';
username = username !== '' ? "?username=" + username : '';
return this.get('badge.url') + username;
}.property("badge", "user")
}); });

View File

@ -1,17 +1,33 @@
import UserBadge from 'discourse/models/user-badge'; import UserBadge from 'discourse/models/user-badge';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
queryParams: ['username'],
noMoreBadges: false, noMoreBadges: false,
userBadges: null, userBadges: null,
needs: ["application"], needs: ["application"],
user: function(){
if (this.get("username")) {
return this.get('userBadges')[0].get('user');
}
}.property("username"),
grantCount: function() {
if (this.get("username")) {
return this.get('userBadges.grant_count');
} else {
return this.get('model.grant_count');
}
}.property('username', 'model', 'userBadges'),
actions: { actions: {
loadMore() { loadMore() {
const self = this; const self = this;
const userBadges = this.get('userBadges'); const userBadges = this.get('userBadges');
UserBadge.findByBadgeId(this.get('model.id'), { UserBadge.findByBadgeId(this.get('model.id'), {
offset: userBadges.length offset: userBadges.length,
username: this.get('username'),
}).then(function(result) { }).then(function(result) {
userBadges.pushObjects(result); userBadges.pushObjects(result);
if(userBadges.length === 0){ if(userBadges.length === 0){
@ -22,11 +38,12 @@ export default Ember.Controller.extend({
}, },
layoutClass: function(){ layoutClass: function(){
var user = this.get("user") ? " single-user" : "";
var ub = this.get("userBadges"); var ub = this.get("userBadges");
if(ub && ub[0] && ub[0].post_id){ if(ub && ub[0] && ub[0].post_id){
return "user-badge-with-posts"; return "user-badge-with-posts" + user;
} else { } else {
return "user-badge-no-posts"; return "user-badge-no-posts" + user;
} }
}.property("userBadges"), }.property("userBadges"),
@ -34,7 +51,7 @@ export default Ember.Controller.extend({
if (this.get('noMoreBadges')) { return false; } if (this.get('noMoreBadges')) { return false; }
if (this.get('userBadges')) { if (this.get('userBadges')) {
return this.get('model.grant_count') > this.get('userBadges.length'); return this.get('grantCount') > this.get('userBadges.length');
} else { } else {
return false; return false;
} }

View File

@ -4,9 +4,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
// You need a value in the field to submit it. // You need a value in the field to submit it.
submitDisabled: function() { submitDisabled: function() {
return Ember.isEmpty(this.get('accountEmailOrUsername').trim()) || this.get('disabled'); return Ember.isEmpty((this.get('accountEmailOrUsername') || '').trim()) || this.get('disabled');
}.property('accountEmailOrUsername', 'disabled'), }.property('accountEmailOrUsername', 'disabled'),
onShow: function() {
if ($.cookie('email')) {
this.set('accountEmailOrUsername', $.cookie('email'));
}
},
actions: { actions: {
submit: function() { submit: function() {
var self = this; var self = this;

View File

@ -6,6 +6,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
// If this isn't defined, it will proxy to the user model on the preferences // If this isn't defined, it will proxy to the user model on the preferences
// page which is wrong. // page which is wrong.
emailOrUsername: null, emailOrUsername: null,
inviteIcon: "envelope",
isAdmin: function(){ isAdmin: function(){
return Discourse.User.currentProp("admin"); return Discourse.User.currentProp("admin");
@ -88,8 +89,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (Ember.isEmpty(this.get('emailOrUsername'))) { if (Ember.isEmpty(this.get('emailOrUsername'))) {
return I18n.t('topic.invite_reply.to_topic_blank'); return I18n.t('topic.invite_reply.to_topic_blank');
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) { } else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
this.set("inviteIcon", "envelope");
return I18n.t('topic.invite_reply.to_topic_email'); return I18n.t('topic.invite_reply.to_topic_email');
} else { } else {
this.set("inviteIcon", "hand-o-right");
return I18n.t('topic.invite_reply.to_topic_username'); return I18n.t('topic.invite_reply.to_topic_username');
} }
} }

View File

@ -32,8 +32,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
} }
}, },
cannotDeleteAccount: Em.computed.not('can_delete_account'), cannotDeleteAccount: Em.computed.not('currentUser.can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), deleteDisabled: Em.computed.or('model.isSaving', 'deleting', 'cannotDeleteAccount'),
canEditName: setting('enable_names'), canEditName: setting('enable_names'),

View File

@ -9,10 +9,10 @@ export default Ember.Controller.extend({
newEmailEmpty: Em.computed.empty('newEmail'), newEmailEmpty: Em.computed.empty('newEmail'),
saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'), saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'),
unchanged: propertyEqual('newEmailLower', 'email'), unchanged: propertyEqual('newEmailLower', 'currentUser.email'),
newEmailLower: function() { newEmailLower: function() {
return this.get('newEmail').toLowerCase(); return this.get('newEmail').toLowerCase().trim();
}.property('newEmail'), }.property('newEmail'),
saveButtonText: function() { saveButtonText: function() {
@ -26,10 +26,10 @@ export default Ember.Controller.extend({
this.set('saving', true); this.set('saving', true);
return this.get('content').changeEmail(this.get('newEmail')).then(function() { return this.get('content').changeEmail(this.get('newEmail')).then(function() {
self.set('success', true); self.set('success', true);
}, function(data) { }, function(e) {
self.setProperties({ error: true, saving: false }); self.setProperties({ error: true, saving: false });
if (data.responseJSON && data.responseJSON.errors && data.responseJSON.errors[0]) { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0]) {
self.set('errorMessage', data.responseJSON.errors[0]); self.set('errorMessage', e.jqXHR.responseJSON.errors[0]);
} else { } else {
self.set('errorMessage', I18n.t('user.change_email.error')); self.set('errorMessage', I18n.t('user.change_email.error'));
} }
@ -38,5 +38,3 @@ export default Ember.Controller.extend({
} }
}); });

View File

@ -6,6 +6,7 @@ import Quote from 'discourse/lib/quote';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import Composer from 'discourse/models/composer'; import Composer from 'discourse/models/composer';
import DiscourseURL from 'discourse/lib/url';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'], needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
@ -17,8 +18,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
queryParams: ['filter', 'username_filters', 'show_deleted'], queryParams: ['filter', 'username_filters', 'show_deleted'],
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'), loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
enteredAt: null, enteredAt: null,
firstPostExpanded: false,
retrying: false, retrying: false,
firstPostExpanded: false,
adminMenuVisible: false, adminMenuVisible: false,
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'), showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
@ -89,11 +90,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
this.set('selectedReplies', []); this.set('selectedReplies', []);
}.on('init'), }.on('init'),
@computed("model.isPrivateMessage", "model.category_id") showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
showCategoryChooser(isPrivateMessage, categoryId) {
const category = Discourse.Category.findById(categoryId); gotoInbox(name) {
const containsMessages = category && category.get("contains_messages"); var url = '/users/' + this.get('currentUser.username_lower') + '/messages';
return !isPrivateMessage && !containsMessages; if (name) {
url = url + '/group/' + name;
}
DiscourseURL.routeTo(url);
}, },
actions: { actions: {
@ -109,12 +113,19 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
this.deleteTopic(); this.deleteTopic();
}, },
archiveMessage() { archiveMessage() {
this.get('model').archiveMessage(); const topic = this.get('model');
topic.archiveMessage().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
}, },
moveToInbox() { moveToInbox() {
this.get('model').moveToInbox(); const topic = this.get('model');
topic.moveToInbox().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
}, },
// Post related methods // Post related methods

View File

@ -1,3 +1,5 @@
import { exportUserArchive } from 'discourse/lib/export-csv';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
userActionType: null, userActionType: null,
needs: ["application", "user"], needs: ["application", "user"],
@ -14,6 +16,21 @@ export default Ember.Controller.extend({
showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded"); showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded");
} }
this.set("controllers.application.showFooter", showFooter); this.set("controllers.application.showFooter", showFooter);
}.observes("userActionType", "model.stream.itemsLoaded") }.observes("userActionType", "model.stream.itemsLoaded"),
actions: {
exportUserArchive() {
bootbox.confirm(
I18n.t("admin.export_csv.user_archive_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {
if (confirmed) {
exportUserArchive();
}
}
);
}
}
}); });

View File

@ -1,4 +1,6 @@
export default Ember.ArrayController.extend({ export default Ember.ArrayController.extend({
needs: ["user"],
user: Em.computed.alias("controllers.user.model"),
sortProperties: ['badge.badge_type.sort_order', 'badge.name'], sortProperties: ['badge.badge_type.sort_order', 'badge.name'],
orderBy: function(ub1, ub2){ orderBy: function(ub1, ub2){
var sr1 = ub1.get('badge.badge_type.sort_order'); var sr1 = ub1.get('badge.badge_type.sort_order');

View File

@ -3,14 +3,23 @@ import Topic from 'discourse/models/topic';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
needs: ["user-topics-list"], needs: ["user-topics-list", "user"],
pmView: false, pmView: false,
viewingSelf: Em.computed.alias("controllers.user.viewingSelf"),
isGroup: Em.computed.equal('pmView', 'groups'), isGroup: Em.computed.equal('pmView', 'groups'),
selected: Em.computed.alias('controllers.user-topics-list.selected'), selected: Em.computed.alias('controllers.user-topics-list.selected'),
bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'), bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'),
mobileView: function() {
return Discourse.Mobile.mobileView;
}.property(),
showNewPM: function(){
return this.get('controllers.user.viewingSelf') &&
Discourse.User.currentProp('can_send_private_messages');
}.property('controllers.user.viewingSelf'),
@computed('selected.@each', 'bulkSelectEnabled') @computed('selected.@each', 'bulkSelectEnabled')
hasSelection(selected, bulkSelectEnabled){ hasSelection(selected, bulkSelectEnabled){
return bulkSelectEnabled && selected && selected.length > 0; return bulkSelectEnabled && selected && selected.length > 0;

View File

@ -0,0 +1,13 @@
export default Ember.Controller.extend({
needs: ['user'],
user: Em.computed.alias('controllers.user.model'),
moreTopics: function(){
return this.get('model.topics').length > 5;
}.property('model'),
moreReplies: function(){
return this.get('model.replies').length > 5;
}.property('model'),
moreBadges: function(){
return this.get('model.badges').length > 5;
}.property('model')
});

View File

@ -14,9 +14,4 @@ export default Ember.Controller.extend({
} }
}, },
showNewPM: function(){
return this.get('controllers.user.viewingSelf') &&
Discourse.User.currentProp('can_send_private_messages');
}.property('controllers.user.viewingSelf')
}); });

View File

@ -1,4 +1,3 @@
import { exportUserArchive } from 'discourse/lib/export-csv';
import CanCheckEmails from 'discourse/mixins/can-check-emails'; import CanCheckEmails from 'discourse/mixins/can-check-emails';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import UserAction from 'discourse/models/user-action'; import UserAction from 'discourse/models/user-action';
@ -89,17 +88,5 @@ export default Ember.Controller.extend(CanCheckEmails, {
.then(user => user.destroy({deletePosts: true})); .then(user => user.destroy({deletePosts: true}));
}, },
exportUserArchive() {
bootbox.confirm(
I18n.t("admin.export_csv.user_archive_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {
if (confirmed) {
exportUserArchive();
}
}
);
}
} }
}); });

View File

@ -0,0 +1,23 @@
/**
Supports Discourse's category hashtags (#category-slug) for automatically
generating a link to the category.
**/
Discourse.Dialect.inlineRegexp({
start: '#',
matcher: /^#([\w-:]{1,50})/i,
spaceOrTagBoundary: true,
emitter: function(matches) {
var slug = matches[1],
hashtag = matches[0],
attributeClass = 'hashtag',
categoryHashtagLookup = this.dialect.options.categoryHashtagLookup,
result = categoryHashtagLookup && categoryHashtagLookup(slug);
if (result && result[0] === "category") {
return ['a', { class: attributeClass, href: result[1] }, '#', ["span", {}, slug]];
} else {
return ['span', { class: attributeClass }, hashtag];
}
}
});

View File

@ -5,22 +5,23 @@
**/ **/
Discourse.Dialect.inlineRegexp({ Discourse.Dialect.inlineRegexp({
start: '@', start: '@',
// NOTE: we really should be using SiteSettings here, but it loads later in process // NOTE: since we can't use SiteSettings here (they loads later in process)
// also, if we do, we must ensure serverside version works as well // we are being less strict to account for more cases than allowed
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9\_])/, matcher: /^@(\w[\w.-]{0,59})\b/i,
wordBoundary: true, wordBoundary: true,
emitter: function(matches) { emitter: function(matches) {
var username = matches[1], var mention = matches[0].trim(),
name = matches[1],
mentionLookup = this.dialect.options.mentionLookup; mentionLookup = this.dialect.options.mentionLookup;
var type = mentionLookup && mentionLookup(username.substr(1)); var type = mentionLookup && mentionLookup(name);
if (type === "user") { if (type === "user") {
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username]; return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + name.toLowerCase()}, mention];
} else if (type === "group") { } else if (type === "group") {
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + username.substr(1)}, username]; return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + name}, mention];
} else { } else {
return ['span', {'class': 'mention'}, username]; return ['span', {'class': 'mention'}, mention];
} }
} }
}); });
@ -34,10 +35,10 @@ Discourse.Dialect.on("parseNode", function(event) {
var parent = path[path.length - 1]; var parent = path[path.length - 1];
// If the parent is an 'a', remove it // If the parent is an 'a', remove it
if (parent && parent[0] === 'a') { if (parent && parent[0] === 'a') {
var username = node[2]; var name = node[2];
node.length = 0; node.length = 0;
node[0] = "__RAW"; node[0] = "__RAW";
node[1] = username; node[1] = name;
} }
} }

View File

@ -9,6 +9,16 @@ function categoryStripe(color, classes) {
return "<span class='" + classes + "' " + style + "></span>"; return "<span class='" + classes + "' " + style + "></span>";
} }
/**
Generates category badge HTML
@param {Object} category The category to generate the badge for.
@param {Object} opts
@param {String} [opts.url] The url that we want the category badge to link to.
@param {Boolean} [opts.allowUncategorized] If false, returns an empty string for the uncategorized category.
@param {Boolean} [opts.link] If false, the category badge will not be a link.
@param {Boolean} [opts.hideParaent] If true, parent category will be hidden in the badge.
**/
export function categoryBadgeHTML(category, opts) { export function categoryBadgeHTML(category, opts) {
opts = opts || {}; opts = opts || {};
@ -21,7 +31,7 @@ export function categoryBadgeHTML(category, opts) {
var description = get(category, 'description_text'), var description = get(category, 'description_text'),
restricted = get(category, 'read_restricted'), restricted = get(category, 'read_restricted'),
url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category), url = opts.url ? opts.url : Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
href = (opts.link === false ? '' : url), href = (opts.link === false ? '' : url),
tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'), tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'),
extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''), extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
@ -50,6 +60,7 @@ export function categoryBadgeHTML(category, opts) {
">"; ">";
var name = escapeExpression(get(category, 'name')); var name = escapeExpression(get(category, 'name'));
if (restricted) { if (restricted) {
html += iconHTML('lock') + " " + name; html += iconHTML('lock') + " " + name;
} else { } else {

View File

@ -24,6 +24,5 @@ registerUnbound('raw', function(templateName, params) {
Ember.warn('Could not find raw template: ' + templateName); Ember.warn('Could not find raw template: ' + templateName);
return; return;
} }
return renderRaw(this, template, templateName, params); return renderRaw(this, template, templateName, params);
}); });

View File

@ -8,9 +8,9 @@ function resolveParams(ctx, options) {
if (options.hashTypes) { if (options.hashTypes) {
Ember.keys(hash).forEach(function(k) { Ember.keys(hash).forEach(function(k) {
const type = options.hashTypes[k]; const type = options.hashTypes[k];
if (type === "STRING") { if (type === "STRING" || type === "StringLiteral") {
params[k] = hash[k]; params[k] = hash[k];
} else if (type === "ID") { } else if (type === "ID" || type === "PathExpression") {
params[k] = get(ctx, hash[k], options); params[k] = get(ctx, hash[k], options);
} }
}); });
@ -23,7 +23,7 @@ function resolveParams(ctx, options) {
export default function registerUnbound(name, fn) { export default function registerUnbound(name, fn) {
const func = function(property, options) { const func = function(property, options) {
if (options.types && options.types[0] === "ID") { if (options.types && (options.types[0] === "ID" || options.types[0] === "PathExpression")) {
property = get(this, property, options); property = get(this, property, options);
} }

View File

@ -282,6 +282,14 @@ export default function(options) {
}, 50); }, 50);
}); });
const checkTriggerRule = (opts) => {
if (options.triggerRule) {
return options.triggerRule(me[0], opts);
} else {
return true;
}
};
$(this).on('keypress.autocomplete', function(e) { $(this).on('keypress.autocomplete', function(e) {
var caretPosition, term; var caretPosition, term;
@ -289,7 +297,7 @@ export default function(options) {
if (options.key && e.which === options.key.charCodeAt(0)) { if (options.key && e.which === options.key.charCodeAt(0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]); caretPosition = Discourse.Utilities.caretPosition(me[0]);
var prevChar = me.val().charAt(caretPosition - 1); var prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || allowedLettersRegex.test(prevChar)) { if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
completeStart = completeEnd = caretPosition; completeStart = completeEnd = caretPosition;
updateAutoComplete(options.dataSource("")); updateAutoComplete(options.dataSource(""));
} }
@ -343,7 +351,7 @@ export default function(options) {
stopFound = prev === options.key; stopFound = prev === options.key;
if (stopFound) { if (stopFound) {
prev = me[0].value[c - 1]; prev = me[0].value[c - 1];
if (!prev || allowedLettersRegex.test(prev)) { if (checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev))) {
completeStart = c; completeStart = c;
caretPosition = completeEnd = initial; caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial); term = me[0].value.substring(c + 1, initial);
@ -351,7 +359,7 @@ export default function(options) {
return true; return true;
} }
} }
prevIsGood = /[a-zA-Z\.]/.test(prev); prevIsGood = /[a-zA-Z\.-]/.test(prev);
} }
} }

View File

@ -0,0 +1,5 @@
export const SEPARATOR = ":";
export function replaceSpan($elem, categorySlug, categoryLink) {
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#<span>${categorySlug}</span></a>`);
};

View File

@ -1,5 +1,10 @@
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
export function isValidLink($link) {
return ($link.hasClass("track-link") ||
$link.closest('.hashtag,.badge-category,.onebox-result,.onebox-body').length === 0);
};
export default { export default {
trackClick(e) { trackClick(e) {
// cancel click if triggered as part of selection. // cancel click if triggered as part of selection.
@ -32,8 +37,7 @@ export default {
var $badge = $('span.badge', $link); var $badge = $('span.badge', $link);
if ($badge.length === 1) { if ($badge.length === 1) {
// don't update counts in category badge nor in oneboxes (except when we force it) // don't update counts in category badge nor in oneboxes (except when we force it)
if ($link.hasClass("track-link") || if (isValidLink($link)) {
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
var html = $badge.html(); var html = $badge.html();
if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); } if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
} }

View File

@ -68,19 +68,32 @@
RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler; RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars"; RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars";
function replaceGet(ast) {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
RawHandlebars.Compiler.prototype.mustache = function(mustache) { visitor.MustacheStatement = function(mustache) {
if ( !(mustache.params.length || mustache.hash)) { if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
var id = new Handlebars.AST.IdNode([{ part: 'get' }]); mustache.path = {
mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, mustache.escaped); type: "PathExpression",
} data: false,
depth: mustache.path.depth,
return Handlebars.Compiler.prototype.mustache.call(this, mustache); parts: ["get"],
original: "get",
loc: mustache.path.loc,
strict: true,
falsy: true
}; };
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
visitor.accept(ast);
}
RawHandlebars.precompile = function(value, asObject) { RawHandlebars.precompile = function(value, asObject) {
var ast = Handlebars.parse(value); var ast = Handlebars.parse(value);
replaceGet(ast);
var options = { var options = {
knownHelpers: { knownHelpers: {
@ -96,9 +109,10 @@
return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject);
}; };
RawHandlebars.compile = function(string) { RawHandlebars.compile = function(string) {
var ast = Handlebars.parse(string); var ast = Handlebars.parse(string);
replaceGet(ast);
// this forces us to rewrite helpers // this forces us to rewrite helpers
var options = { data: true, stringParams: true }; var options = { data: true, stringParams: true };
var environment = new RawHandlebars.Compiler().compile(ast, options); var environment = new RawHandlebars.Compiler().compile(ast, options);

View File

@ -0,0 +1,51 @@
import { replaceSpan } from 'discourse/lib/category-hashtags';
const validCategoryHashtags = {};
const checkedCategoryHashtags = [];
const testedKey = 'tested';
const testedClass = `hashtag-${testedKey}`;
function updateFound($hashtags, categorySlugs) {
Ember.run.schedule('afterRender', () => {
$hashtags.each((index, hashtag) => {
const categorySlug = categorySlugs[index];
const link = validCategoryHashtags[categorySlug];
const $hashtag = $(hashtag);
if (link) {
replaceSpan($hashtag, categorySlug, link);
} else if (checkedCategoryHashtags.indexOf(categorySlug) !== -1) {
$hashtag.addClass(testedClass);
}
});
});
};
export function linkSeenCategoryHashtags($elem) {
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
const unseen = [];
if ($hashtags.length) {
const categorySlugs = $hashtags.map((_, hashtag) => $(hashtag).text().substr(1));
if (categorySlugs.length) {
_.uniq(categorySlugs).forEach((categorySlug) => {
if (checkedCategoryHashtags.indexOf(categorySlug) === -1) {
unseen.push(categorySlug);
}
});
}
updateFound($hashtags, categorySlugs);
}
return unseen;
};
export function fetchUnseenCategoryHashtags(categorySlugs) {
return Discourse.ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } })
.then((response) => {
response.valid.forEach((category) => {
validCategoryHashtags[category.slug] = category.url;
});
checkedCategoryHashtags.push.apply(checkedCategoryHashtags, categorySlugs);
});
}

View File

@ -239,6 +239,7 @@ Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
Discourse.Markdown.whiteListTag('a', 'class', 'onebox'); Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
Discourse.Markdown.whiteListTag('a', 'class', 'mention'); Discourse.Markdown.whiteListTag('a', 'class', 'mention');
Discourse.Markdown.whiteListTag('a', 'class', 'mention-group'); Discourse.Markdown.whiteListTag('a', 'class', 'mention-group');
Discourse.Markdown.whiteListTag('a', 'class', 'hashtag');
Discourse.Markdown.whiteListTag('a', 'target', '_blank'); Discourse.Markdown.whiteListTag('a', 'target', '_blank');
Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow'); Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow');
@ -251,6 +252,7 @@ Discourse.Markdown.whiteListTag('div', 'class', 'title');
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls'); Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
Discourse.Markdown.whiteListTag('span', 'class', 'mention'); Discourse.Markdown.whiteListTag('span', 'class', 'mention');
Discourse.Markdown.whiteListTag('span', 'class', 'hashtag');
Discourse.Markdown.whiteListTag('aside', 'class', 'quote'); Discourse.Markdown.whiteListTag('aside', 'class', 'quote');
Discourse.Markdown.whiteListTag('aside', 'data-*'); Discourse.Markdown.whiteListTag('aside', 'data-*');

View File

@ -143,6 +143,19 @@ Discourse.Utilities = {
return String(text).trim(); return String(text).trim();
}, },
// Determine the row and col of the caret in an element
caretRowCol: function(el) {
var caretPosition = Discourse.Utilities.caretPosition(el);
var rows = el.value.slice(0, caretPosition).split("\n");
var rowNum = rows.length;
var colNum = caretPosition - rows.splice(0, rowNum - 1).reduce(function(sum, row) {
return sum + row.length + 1;
}, 0);
return { rowNum: rowNum, colNum: colNum};
},
// Determine the position of the caret in an element // Determine the position of the caret in an element
caretPosition: function(el) { caretPosition: function(el) {
var r, rc, re; var r, rc, re;
@ -249,7 +262,11 @@ Discourse.Utilities = {
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">'; return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav)$/i).test(upload.original_filename)) { } else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav)$/i).test(upload.original_filename)) {
// is Audio/Video // is Audio/Video
if (Discourse.CDN) {
return Discourse.CDN.startsWith('//') ? "http:" + Discourse.getURLWithCDN(upload.url) : Discourse.getURLWithCDN(upload.url);
} else {
return "http://" + Discourse.BaseUrl + upload.url; return "http://" + Discourse.BaseUrl + upload.url;
}
} else { } else {
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')'; return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')';
} }

View File

@ -8,7 +8,7 @@ export default RestModel.extend({
return this.get('name').toLowerCase().replace(/\s/g, '_'); return this.get('name').toLowerCase().replace(/\s/g, '_');
}, },
@computed @computed('name')
displayName() { displayName() {
const i18nKey = `badges.badge_grouping.${this.get('i18nNameKey')}.name`; const i18nKey = `badges.badge_grouping.${this.get('i18nNameKey')}.name`;
return I18n.t(i18nKey, {defaultValue: this.get('name')}); return I18n.t(i18nKey, {defaultValue: this.get('name')});

View File

@ -5,6 +5,10 @@ const Badge = RestModel.extend({
newBadge: Em.computed.none('id'), newBadge: Em.computed.none('id'),
url: function() {
return Discourse.getURL(`/badges/${this.get('id')}/${this.get('slug')}`);
}.property(),
/** /**
@private @private
@ -159,7 +163,7 @@ Badge.reopenClass({
let badges = []; let badges = [];
if ("badge" in json) { if ("badge" in json) {
badges = [json.badge]; badges = [json.badge];
} else { } else if (json.badges) {
badges = json.badges; badges = json.badges;
} }
badges = badges.map(function(badgeJson) { badges = badges.map(function(badgeJson) {
@ -207,4 +211,3 @@ Badge.reopenClass({
}); });
export default Badge; export default Badge;

View File

@ -86,8 +86,7 @@ const Category = RestModel.extend({
allow_badges: this.get('allow_badges'), allow_badges: this.get('allow_badges'),
custom_fields: this.get('custom_fields'), custom_fields: this.get('custom_fields'),
topic_template: this.get('topic_template'), topic_template: this.get('topic_template'),
suppress_from_homepage: this.get('suppress_from_homepage'), suppress_from_homepage: this.get('suppress_from_homepage')
contains_messages: this.get("contains_messages"),
}, },
type: this.get('id') ? 'PUT' : 'POST' type: this.get('id') ? 'PUT' : 'POST'
}); });
@ -205,14 +204,14 @@ Category.reopenClass({
return _uncategorized; return _uncategorized;
}, },
slugFor(category) { slugFor(category, separator = "/") {
if (!category) return ""; if (!category) return "";
const parentCategory = Em.get(category, 'parentCategory'); const parentCategory = Em.get(category, 'parentCategory');
let result = ""; let result = "";
if (parentCategory) { if (parentCategory) {
result = Category.slugFor(parentCategory) + "/"; result = Category.slugFor(parentCategory) + separator;
} }
const id = Em.get(category, 'id'), const id = Em.get(category, 'id'),
@ -285,6 +284,64 @@ Category.reopenClass({
reloadById(id) { reloadById(id) {
return Discourse.ajax(`/c/${id}/show.json`); return Discourse.ajax(`/c/${id}/show.json`);
},
search(term, opts) {
var limit = 5;
if (opts) {
if (opts.limit === 0) {
return [];
} else if (opts.limit) {
limit = opts.limit;
}
}
const emptyTerm = (term === "");
let slugTerm = term;
if (!emptyTerm) {
term = term.toLowerCase();
slugTerm = term;
term = term.replace(/-/g, " ");
}
const categories = Category.listByActivity();
const length = categories.length;
var i;
var data = [];
const done = () => {
return data.length === limit;
};
for (i = 0; i < length && !done(); i++) {
const category = categories[i];
if ((emptyTerm && !category.get('parent_category_id')) ||
(!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) === 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) === 0))) {
data.push(category);
}
}
if (!done()) {
for (i = 0; i < length && !done(); i++) {
const category = categories[i];
if (!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) > 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) > 0)) {
if (data.indexOf(category) === -1) data.push(category);
}
}
}
return _.sortBy(data, (category) => {
return category.get('read_restricted');
});
} }
}); });

View File

@ -67,17 +67,16 @@ const Composer = RestModel.extend({
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
@computed("privateMessage", "archetype.hasOptions", "categoryId") @computed("privateMessage", "archetype.hasOptions")
showCategoryChooser(isPrivateMessage, hasOptions, categoryId) { showCategoryChooser(isPrivateMessage, hasOptions) {
const manyCategories = Discourse.Category.list().length > 1; const manyCategories = Discourse.Category.list().length > 1;
const category = Discourse.Category.findById(categoryId); return !isPrivateMessage && (hasOptions || manyCategories);
const containsMessages = category && category.get("contains_messages");
return !isPrivateMessage && !containsMessages && (hasOptions || manyCategories);
}, },
privateMessage: function(){ @computed("creatingPrivateMessage", "topic")
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message'; privateMessage(creatingPrivateMessage, topic) {
}.property('creatingPrivateMessage', 'topic'), return creatingPrivateMessage || (topic && topic.get('archetype') === 'private_message');
},
topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'), topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'),

View File

@ -424,8 +424,12 @@ const Topic = RestModel.extend({
this.set("archiving", true); this.set("archiving", true);
var promise = Discourse.ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'}); var promise = Discourse.ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'});
promise.then(()=>this.set('message_archived', true)) promise.then((msg)=> {
.finally(()=>this.set('archiving', false)); this.set('message_archived', true);
if (msg && msg.group_name) {
this.set('inboxGroupName', msg.group_name);
}
}).finally(()=>this.set('archiving', false));
return promise; return promise;
}, },
@ -434,8 +438,12 @@ const Topic = RestModel.extend({
this.set("archiving", true); this.set("archiving", true);
var promise = Discourse.ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'}); var promise = Discourse.ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'});
promise.then(()=>this.set('message_archived', false)) promise.then((msg)=> {
.finally(()=>this.set('archiving', false)); this.set('message_archived', false);
if (msg && msg.group_name) {
this.set('inboxGroupName', msg.group_name);
}
}).finally(()=>this.set('archiving', false));
return promise; return promise;
} }

View File

@ -48,7 +48,7 @@ UserBadge.reopenClass({
if ("user_badge" in json) { if ("user_badge" in json) {
userBadges = [json.user_badge]; userBadges = [json.user_badge];
} else { } else {
userBadges = json.user_badges; userBadges = (json.user_badge_info && json.user_badge_info.user_badges) || json.user_badges;
} }
userBadges = userBadges.map(function(userBadgeJson) { userBadges = userBadges.map(function(userBadgeJson) {
@ -73,6 +73,10 @@ UserBadge.reopenClass({
if ("user_badge" in json) { if ("user_badge" in json) {
return userBadges[0]; return userBadges[0];
} else { } else {
if (json.user_badge_info) {
userBadges.grant_count = json.user_badge_info.grant_count;
userBadges.username = json.user_badge_info.username;
}
return userBadges; return userBadges;
} }
}, },

View File

@ -10,6 +10,7 @@ import UserBadge from 'discourse/models/user-badge';
import UserActionStat from 'discourse/models/user-action-stat'; import UserActionStat from 'discourse/models/user-action-stat';
import UserAction from 'discourse/models/user-action'; import UserAction from 'discourse/models/user-action';
import Group from 'discourse/models/group'; import Group from 'discourse/models/group';
import Topic from 'discourse/models/topic';
const User = RestModel.extend({ const User = RestModel.extend({
@ -355,6 +356,38 @@ const User = RestModel.extend({
}); });
} }
}); });
},
summary() {
return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`)
.then(json => {
const topicMap = {};
json.topics.forEach(t => {
topicMap[t.id] = Topic.create(t);
});
const badgeMap = {};
Badge.createFromJson(json).forEach(b => {
badgeMap[b.id] = b;
});
const summary = json["user_summary"];
summary.replies.forEach(r => {
r.topic = topicMap[r.topic_id];
r.url = r.topic.urlForPostNumber(r.post_number);
r.createdAt = new Date(r.created_at);
});
summary.topics = summary.topic_ids.map(id => topicMap[id]);
summary.badges = summary.badges.map(ub => {
const badge = badgeMap[ub.badge_id];
badge.count = ub.count;
return badge;
});
return summary;
});
} }
}); });

View File

@ -42,6 +42,7 @@ export default function() {
this.route('parentCategory', { path: '/c/:slug' }); this.route('parentCategory', { path: '/c/:slug' });
this.route('categoryNone', { path: '/c/:slug/none' }); this.route('categoryNone', { path: '/c/:slug/none' });
this.route('category', { path: '/c/:parentSlug/:slug' }); this.route('category', { path: '/c/:parentSlug/:slug' });
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
// homepage // homepage
this.route(Discourse.Utilities.defaultHomepage(), { path: '/' }); this.route(Discourse.Utilities.defaultHomepage(), { path: '/' });
@ -57,11 +58,13 @@ export default function() {
// User routes // User routes
this.resource('users'); this.resource('users');
this.resource('user', { path: '/users/:username' }, function() { this.resource('user', { path: '/users/:username' }, function() {
this.route('summary');
this.resource('userActivity', { path: '/activity' }, function() { this.resource('userActivity', { path: '/activity' }, function() {
this.route('topics'); this.route('topics');
this.route('replies'); this.route('replies');
this.route('likesGiven', {path: 'likes-given'}); this.route('likesGiven', {path: 'likes-given'});
this.route('bookmarks'); this.route('bookmarks');
this.route('pending');
}); });
this.resource('userNotifications', {path: '/notifications'}, function(){ this.resource('userNotifications', {path: '/notifications'}, function(){

View File

@ -2,6 +2,11 @@ import UserBadge from 'discourse/models/user-badge';
import Badge from 'discourse/models/badge'; import Badge from 'discourse/models/badge';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
queryParams: {
username: {
refreshModel: true
}
},
actions: { actions: {
didTransition() { didTransition() {
this.controllerFor("badges/show")._showFooter(); this.controllerFor("badges/show")._showFooter();
@ -24,10 +29,13 @@ export default Discourse.Route.extend({
} }
}, },
afterModel(model) { afterModel(model,transition) {
return UserBadge.findByBadgeId(model.get("id")).then(userBadges => { const username = transition.queryParams && transition.queryParams.username;
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
this.userBadges = userBadges; this.userBadges = userBadges;
}); });
}, },
titleToken() { titleToken() {

View File

@ -0,0 +1,11 @@
import Category from 'discourse/models/category';
export default Discourse.Route.extend({
model: function(params) {
return Category.findById(params.id);
},
redirect: function(model) {
this.transitionTo(`/c/${Category.slugFor(model)}`);
}
});

View File

@ -1,2 +0,0 @@
export default Discourse.Route.extend({
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return this.modelFor("User").summary();
}
});

View File

@ -9,7 +9,9 @@
<div class='row'> <div class='row'>
<div class='badge'>{{user-badge badge=model}}</div> <div class='badge'>{{user-badge badge=model}}</div>
<div class='description'>{{{model.displayDescriptionHtml}}}</div> <div class='description'>{{{model.displayDescriptionHtml}}}</div>
<div class='grant-count'>{{i18n 'badges.granted' count=model.grant_count}}</div> {{#unless user}}
<div class='grant-count'>{{i18n 'badges.granted' count=grantCount}}</div>
{{/unless}}
<div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}} <div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
</div> </div>
</div> </div>
@ -22,10 +24,27 @@
</div> </div>
{{/if}} {{/if}}
{{#if user}}
<div class='badge-user-info'>
{{#link-to 'user' user}}
{{avatar user imageSize="extra_large"}}
<div class="details clearfix">
{{poster-name post=user}}
</div>
{{/link-to}}
<div class='earned'>
{{i18n 'badges.earned_n_times' count=grantCount}}
</div>
</div>
{{/if}}
{{#if userBadges}} {{#if userBadges}}
<div class={{unbound layoutClass}}> <div class={{unbound layoutClass}}>
{{#each ub in userBadges}} {{#each ub in userBadges}}
<div class="badge-user"> <div class="badge-user">
{{#if user}}
{{format-date ub.granted_at}}
{{else}}
{{#link-to 'user' ub.user classNames="badge-info"}} {{#link-to 'user' ub.user classNames="badge-info"}}
{{avatar ub.user imageSize="large"}} {{avatar ub.user imageSize="large"}}
<div class="details"> <div class="details">
@ -33,12 +52,20 @@
{{format-date ub.granted_at}} {{format-date ub.granted_at}}
</div> </div>
{{/link-to}} {{/link-to}}
{{/if}}
{{#if ub.post_number}} {{#if ub.post_number}}
<a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a> <a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a>
{{/if}} {{/if}}
</div> </div>
{{/each}} {{/each}}
{{#unless canLoadMore}}
{{#if user}}
<a class='load-more' href='{{model.url}}'>{{i18n 'badges.more_with_badge'}}</a>
{{/if}}
{{/unless}}
</div> </div>
{{conditional-loading-spinner condition=canLoadMore}} {{conditional-loading-spinner condition=canLoadMore}}

View File

@ -0,0 +1 @@
<input class="date-picker">

View File

@ -20,18 +20,13 @@
</section> </section>
{{#if emailInEnabled}} {{#if emailInEnabled}}
<section class='field'>
<label>
{{input type="checkbox" checked=category.contains_messages}}
{{i18n 'category.contains_messages'}}
</label>
</section>
<section class='field'> <section class='field'>
<label> <label>
{{input type="checkbox" checked=category.email_in_allow_strangers}} {{input type="checkbox" checked=category.email_in_allow_strangers}}
{{i18n 'category.email_in_allow_strangers'}} {{i18n 'category.email_in_allow_strangers'}}
</label> </label>
</section> </section>
<section class='field'> <section class='field'>
<label> <label>
{{fa-icon "envelope-o"}} {{fa-icon "envelope-o"}}

View File

@ -4,7 +4,7 @@
<div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}> <div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}>
<ul> <ul>
{{#each p in site.periods}} {{#each p in site.periods}}
<li><a href {{action "changePeriod" p}}>{{period-title p}}</a></li> <li><a href {{action "changePeriod" p}}>{{period-title p showDateRange=true}}</a></li>
{{/each}} {{/each}}
</ul> </ul>
</div> </div>

View File

@ -1,7 +1,7 @@
{{#link-to 'badges.show' badge}} <a href="{{badgeUrl}}">
{{#badge-button badge=badge}} {{#badge-button badge=badge}}
{{#if showGrantCount}} {{#if showGrantCount}}
<span class="count">(&times;&nbsp;{{count}})</span> <span class="count">(&times;&nbsp;{{count}})</span>
{{/if}} {{/if}}
{{/badge-button}} {{/badge-button}}
{{/link-to}} </a>

View File

@ -1,4 +1,5 @@
<div class="container"> <div class="container user-table">
<div class="wrapper">
<section class='user-navigation'> <section class='user-navigation'>
<ul class='action-list nav-stacked'> <ul class='action-list nav-stacked'>
{{#each tabs as |tab|}} {{#each tabs as |tab|}}
@ -22,4 +23,5 @@
{{outlet}} {{outlet}}
</section> </section>
</section> </section>
</div>
</div> </div>

View File

@ -28,7 +28,7 @@
{{#if model.finished}} {{#if model.finished}}
{{d-button class="btn-primary" action="closeModal" label="close"}} {{d-button class="btn-primary" action="closeModal" label="close"}}
{{else}} {{else}}
{{d-button icon="envelope" action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}} {{d-button icon=inviteIcon action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}}
{{#if showCopyInviteButton}} {{#if showCopyInviteButton}}
{{d-button icon="link" action="generateInvitelink" class="btn-primary" disabled=disabledCopyLink label='user.invited.generate_link'}} {{d-button icon="link" action="generateInvitelink" class="btn-primary" disabled=disabledCopyLink label='user.invited.generate_link'}}
{{/if}} {{/if}}

View File

@ -68,7 +68,7 @@
{{#if showBadges}} {{#if showBadges}}
<div class="badge-section"> <div class="badge-section">
{{#each ub in user.featured_user_badges}} {{#each ub in user.featured_user_badges}}
{{user-badge badge=ub.badge}} {{user-badge badge=ub.badge user=user}}
{{/each}} {{/each}}
{{#if showMoreBadges}} {{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}} {{#link-to 'user.badges' user class="btn more-user-badges"}}

View File

@ -1,5 +1,5 @@
<section class='user-navigation'> <section class='user-navigation'>
<ul class='action-list nav-stacked'> <ul class='action-list activity-list nav-stacked'>
<li class='no-glyph'> <li class='no-glyph'>
{{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}} {{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}}

View File

@ -1,5 +1,5 @@
<section class='user-content user-badges-list'> <section class='user-content user-badges-list'>
{{#each ub in controller}} {{#each ub in controller}}
{{user-badge badge=ub.badge count=ub.count}} {{user-badge badge=ub.badge count=ub.count user=user}}
{{/each}} {{/each}}
</section> </section>

View File

@ -1,4 +1,9 @@
<section class='user-navigation'> <section class='user-navigation'>
{{#unless mobileView}}
{{#if showNewPM}}
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
{{/if}}
{{/unless}}
<ul class='action-list nav-stacked'> <ul class='action-list nav-stacked'>
<li class="noGlyph"> <li class="noGlyph">
{{#link-to 'userPrivateMessages.index' model}} {{#link-to 'userPrivateMessages.index' model}}
@ -23,7 +28,7 @@
{{capitalize group.name}} {{capitalize group.name}}
{{/link-to}} {{/link-to}}
</li> </li>
<li> <li class='archive'>
{{#link-to 'userPrivateMessages.groupArchive' group.name}} {{#link-to 'userPrivateMessages.groupArchive' group.name}}
{{i18n 'user.messages.archive'}} {{i18n 'user.messages.archive'}}
{{/link-to}} {{/link-to}}
@ -32,7 +37,6 @@
{{/each}} {{/each}}
</ul> </ul>
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
</section> </section>
<section class='user-right messages'> <section class='user-right messages'>
@ -42,6 +46,12 @@
<i class="fa fa-list"></i> <i class="fa fa-list"></i>
</button> </button>
{{#if mobileView}}
{{#if showNewPM}}
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
{{/if}}
{{/if}}
{{#if canArchive}} {{#if canArchive}}
<button {{action "archive"}} class="btn btn-archive"> <button {{action "archive"}} class="btn btn-archive">
{{i18n "user.messages.archive"}} {{i18n "user.messages.archive"}}

View File

@ -1,6 +1,6 @@
<section class='user-navigation'> <section class='user-navigation'>
<ul class='action-list nav-stacked'> <ul class='notification-list action-list nav-stacked'>
{{#if model}} {{#if model}}
<li class='no-glyph'> <li class='no-glyph'>
{{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}} {{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}}

View File

@ -178,7 +178,7 @@
{{/if}} {{/if}}
{{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}} {{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}}
{{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}} {{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}}
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}} <span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}</span>
{{preference-checkbox labelKey="user.email_always" checked=model.email_always}} {{preference-checkbox labelKey="user.email_always" checked=model.email_always}}
<div class='instructions'> <div class='instructions'>

View File

@ -0,0 +1,62 @@
{{#if model.replies.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_replies"}}</h3>
{{#each reply in model.replies}}
<ul>
<li>
<a href="{{reply.url}}">{{reply.topic.title}}</a> {{#if reply.like_count}}<span class='like-count'>{{reply.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date reply.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreReplies}}
{{#link-to "userActivity.replies" user class="more"}}{{i18n "user.summary.more_replies"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
{{#if model.topics.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_topics"}}</h3>
{{#each topic in model.topics}}
<ul>
<li>
<a href="{{topic.url}}">{{topic.title}}</a> {{#if topic.like_count}}<span class='like-count'>{{topic.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date topic.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreTopics}}
{{#link-to "userActivity.topics" user class="more"}}{{i18n "user.summary.more_topics"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
<div class='top-section stats-section'>
<h3>{{i18n "user.summary.stats"}}</h3>
<dl>
<dt>{{i18n "user.summary.topic_count"}}</dt>
<dd>{{model.topic_count}}</dd>
<dt>{{i18n "user.summary.post_count"}}</dt>
<dd>{{model.post_count}}</dd>
<dt>{{i18n "user.summary.likes_given"}}</dt>
<dd>{{model.likes_given}}</dd>
<dt>{{i18n "user.summary.likes_received"}}</dt>
<dd>{{model.likes_received}}</dd>
<dt>{{i18n "user.summary.days_visited"}}</dt>
<dd>{{model.days_visited}}</dd>
<dt>{{i18n "user.summary.posts_read_count"}}</dt>
<dd>{{model.posts_read_count}}</dd>
</dl>
</div>
{{#if model.badges.length}}
<div class='top-section badges-section'>
<h3>{{i18n "user.summary.top_badges"}}</h3>
{{#each badge in model.badges}}
{{user-badge badge=badge count=badge.count user=user}}
{{/each}}
{{#if moreBadges}}
{{#link-to "user.badges" user class="more"}}{{i18n "user.summary.more_badges"}}{{/link-to}}
{{/if}}
</div>
{{/if}}

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