Skip to content

Commit

Permalink
FEATURE: Added UI for adding and removing watched and muted categories
Browse files Browse the repository at this point in the history
  • Loading branch information
SamSaffron committed Jan 2, 2014
1 parent 1b259c5 commit 2da5d23
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Discourse.CategoryGroupComponent = Ember.Component.extend({

didInsertElement: function(){
var self = this;

this.$('input').autocomplete({
items: this.get('categories'),
single: false,
allowAny: false,
dataSource: function(term){
return Discourse.Category.list().filter(function(category){
var regex = new RegExp(term, "i");
return category.get("name").match(regex) &&
!_.contains(self.get('categories'), category);
});
},
onChangeItems: function(items) {
self.set("categories", items);
},
template: Discourse.CategoryGroupComponent.templateFunction(),
transformComplete: function(category){
return Discourse.HTML.categoryLink(category);
}
});
}

});

Discourse.CategoryGroupComponent.reopenClass({
templateFunction: function(){
this.compiled = this.compiled || Handlebars.compile("<div class='autocomplete'>" +
"<ul>" +
"{{#each options}}" +
"<li>" +
"{{categoryLinkRaw this}}" +
"</li>" +
"{{/each}}" +
"</ul>" +
"</div>");
return this.compiled;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Handlebars.registerHelper('categoryLink', function(property, options) {
return categoryLinkHTML(Ember.Handlebars.get(this, property, options), options);
});

Handlebars.registerHelper('categoryLinkRaw', function(property, options) {
return categoryLinkHTML(property, options);
});

/**
Produces a bound link to a category
Expand Down
20 changes: 15 additions & 5 deletions app/assets/javascripts/discourse/lib/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ $.fn.autocomplete = function(options) {
var isInput = this[0].tagName === "INPUT";
var inputSelectedItems = [];


var closeAutocomplete = function() {
if (div) {
div.hide().remove();
Expand All @@ -93,7 +92,7 @@ $.fn.autocomplete = function(options) {
// dump what we have in single mode, just in case
inputSelectedItems = [];
}
var d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='fa fa-times'></i></a></span></div>");
var d = $("<div class='item'><span>" + (transformed || item) + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>");
var prev = me.parent().find('.item:last');
if (prev.length === 0) {
me.parent().prepend(d);
Expand Down Expand Up @@ -158,6 +157,11 @@ $.fn.autocomplete = function(options) {
addInputSelectedItem(x);
}
});
if(options.items) {
_.each(options.items, function(item){
addInputSelectedItem(item);
});
}
this.val("");
completeStart = 0;
wrap.click(function() {
Expand Down Expand Up @@ -225,8 +229,14 @@ $.fn.autocomplete = function(options) {
};

var updateAutoComplete = function(r) {

if (completeStart === null) return;

if (r && r.then && typeof(r.then) === "function") {
r.then(updateAutoComplete);
return;
}

autocompleteOptions = r;
if (!r || r.length === 0) {
closeAutocomplete();
Expand Down Expand Up @@ -257,7 +267,7 @@ $.fn.autocomplete = function(options) {
if (!prevChar || /\s/.test(prevChar)) {
completeStart = completeEnd = caretPosition;
var term = "";
options.dataSource(term).then(updateAutoComplete);
updateAutoComplete(options.dataSource(term));
}
}
});
Expand Down Expand Up @@ -304,7 +314,7 @@ $.fn.autocomplete = function(options) {
completeStart = c;
caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial);
options.dataSource(term).then(updateAutoComplete);
updateAutoComplete(options.dataSource(term));
return true;
}
}
Expand Down Expand Up @@ -395,7 +405,7 @@ $.fn.autocomplete = function(options) {
}
}

options.dataSource(term).then(updateAutoComplete);
updateAutoComplete(options.dataSource(term));
return true;
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/assets/javascripts/discourse/models/category.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ Discourse.Category.reopenClass({
});
},

// TODO: optimise, slow for no real reason
findById: function(id){
return Discourse.Category.list().findBy('id', id);
},

findBySlug: function(slug, parentSlug) {

var categories = Discourse.Category.list(),
Expand Down
25 changes: 20 additions & 5 deletions app/assets/javascripts/discourse/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ Discourse.User = Discourse.Model.extend({
**/
save: function() {
var user = this;
return Discourse.ajax("/users/" + this.get('username_lower'), {
data: this.getProperties('auto_track_topics_after_msecs',
var data = this.getProperties('auto_track_topics_after_msecs',
'bio_raw',
'website',
'name',
Expand All @@ -181,7 +180,12 @@ Discourse.User = Discourse.Model.extend({
'new_topic_duration_minutes',
'external_links_in_new_tab',
'watch_new_topics',
'enable_quoting'),
'enable_quoting');
data.watched_category_ids = this.get('watchedCategories').map(function(c){ return c.get('id')});
data.muted_category_ids = this.get('mutedCategories').map(function(c){ return c.get('id')});

return Discourse.ajax("/users/" + this.get('username_lower'), {
data: data,
type: 'PUT'
}).then(function(data) {
user.set('bio_excerpt',data.user.bio_excerpt);
Expand Down Expand Up @@ -350,8 +354,19 @@ Discourse.User = Discourse.Model.extend({
}
}
return Discourse.Utilities.defaultHomepage();
}.property("trust_level", "hasBeenSeenInTheLastMonth")

}.property("trust_level", "hasBeenSeenInTheLastMonth"),

updateMutedCategories: function() {
this.set("mutedCategories", _.map(this.muted_category_ids, function(id){
return Discourse.Category.findById(id);
}));
}.observes("muted_category_ids"),

updateWatchedCategories: function() {
this.set("watchedCategories", _.map(this.watched_category_ids, function(id){
return Discourse.Category.findById(id);
}));
}.observes("watched_category_ids")
});

Discourse.User.reopenClass(Discourse.Singleton, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input class='category-group' type='text'>
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@

</div>

<div class="control-group category">
<label class="control-label">{{i18n user.categories_settings}}</label>
<div class="controls">
<label>{{i18n user.watched_categories}}</label>
{{category-group categories=watchedCategories}}
<div class="instructions">{{i18n user.watched_categories_instructions}}</div>
</div>
<div class="controls">
<label>{{i18n user.muted_categories}}</label>
{{category-group categories=mutedCategories}}
<div class="instructions">{{i18n user.muted_categories_instructions}}</div>
</div>
</div>

<div class="control-group">
<div class="controls">
<button {{action save}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/desktop/compose.scss
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ div.ac-wrap {
line-height: 22px;
vertical-align: bottom;
}
a {
a.remove {
margin-left: 4px;
font-size: 10px;
line-height: 10px;
Expand Down
13 changes: 13 additions & 0 deletions app/assets/stylesheets/desktop/user.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
@import "common/foundation/mixins";

.user-preferences {
input.category-group {
width: 500px;
}

.autocomplete .badge-category {
margin: 2px;
font-weight: normal;
}

.autocomplete .badge-category.selected {
font-weight: bold;
}

textarea {
width: 530px;
height: 100px;
Expand Down
28 changes: 28 additions & 0 deletions app/models/category_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ class CategoryUser < ActiveRecord::Base
belongs_to :category
belongs_to :user

def self.lookup(user, level)
self.where(user: user, notification_level: notification_levels[level])
end

# same for now
def self.notification_levels
TopicUser.notification_levels
Expand All @@ -15,6 +19,21 @@ def self.auto_watch_new_topic(topic)
)
end

def self.batch_set(user, level, category_ids)
records = CategoryUser.where(user: user, notification_level: notification_levels[level])

old_ids = records.pluck(:category_id)

remove = (old_ids - category_ids)
if remove.present?
records.where('category_id in (?)', remove).destroy_all
end

(category_ids - old_ids).each do |id|
CategoryUser.create!(user: user, category_id: id, notification_level: notification_levels[level])
end
end

def self.auto_mute_new_topic(topic)
apply_default_to_topic(
topic,
Expand All @@ -23,6 +42,15 @@ def self.auto_mute_new_topic(topic)
)
end

def notification_level1=(val)
val = Symbol === val ? CategoryUser.notification_levels[val] : val
attributes[:notification_level] = val
end

def notification_level1
attributes[:notification_level]
end

private

def self.apply_default_to_topic(topic, level, reason)
Expand Down
12 changes: 11 additions & 1 deletion app/serializers/user_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ def bio_excerpt
:use_uploaded_avatar,
:has_uploaded_avatar,
:gravatar_template,
:uploaded_avatar_template
:uploaded_avatar_template,
:muted_category_ids,
:watched_category_ids


def auto_track_topics_after_msecs
Expand Down Expand Up @@ -101,8 +103,16 @@ def include_suspended?
def include_suspend_reason?
object.suspended?
end

def include_suspended_till?
object.suspended?
end

def muted_category_ids
CategoryUser.lookup(object, :muted).pluck(:category_id)
end

def watched_category_ids
CategoryUser.lookup(object, :watching).pluck(:category_id)
end
end
10 changes: 9 additions & 1 deletion app/services/user_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ def update(attributes = {})
user.name = attributes[:name] || user.name
user.digest_after_days = attributes[:digest_after_days] || user.digest_after_days

if ids = attributes[:watched_category_ids]
CategoryUser.batch_set(user, :watching, ids)
end

if ids = attributes[:muted_category_ids]
CategoryUser.batch_set(user, :muted, ids)
end

if attributes[:auto_track_topics_after_msecs]
user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i
user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i
end

if attributes[:new_topic_duration_minutes]
Expand Down
5 changes: 5 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ en:
suspended_notice: "This user is suspended until {{date}}."
suspended_reason: "Reason: "
watch_new_topics: "Automatically watch all new topics posted on the forum"
watched_categories: "Watched"
watched_categories_instructions: "You will automatically watch all topics in these categories"
muted_categories: "Muted"
muted_categories_instructions: "You will automatically mute all topics in these categories"

messages:
all: "All"
Expand Down Expand Up @@ -313,6 +317,7 @@ en:
email_always: "Receive email notifications and email digests even if I am active on the forum"

other_settings: "Other"
categories_settings: "Categories"

new_topic_duration:
label: "Consider topics new when"
Expand Down
19 changes: 19 additions & 0 deletions spec/models/category_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
require_dependency 'post_creator'

describe CategoryUser do

it 'allows batch set' do
user = Fabricate(:user)
category1 = Fabricate(:category)
category2 = Fabricate(:category)

watching = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:watching])

CategoryUser.batch_set(user, :watching, [category1.id, category2.id])
watching.pluck(:category_id).sort.should == [category1.id, category2.id]

CategoryUser.batch_set(user, :watching, [])
watching.count.should == 0

CategoryUser.batch_set(user, :watching, [category2.id])
watching.count.should == 1
end


context 'integration' do
before do
ActiveRecord::Base.observers.enable :all
Expand Down

0 comments on commit 2da5d23

Please sign in to comment.