diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index c9dd265c4..d37c428d6 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -2,6 +2,7 @@ import { setting } from 'discourse/lib/computed'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import computed from "ember-addons/ember-computed-decorators"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; export default Ember.Controller.extend(CanCheckEmails, { @@ -134,6 +135,25 @@ export default Ember.Controller.extend(CanCheckEmails, { this.set('saved', false); const model = this.get('model'); + + + // watched status changes warn user + const changedWatch = model.changedCategoryNotifications("watched"); + + if (changedWatch.remove.length > 0 && !this.get("warnedRemoveWatch")) { + var categories = Discourse.Category.findByIds(changedWatch.remove).map((cat) => { + return categoryBadgeHTML(cat); + }).join(" "); + bootbox.confirm(I18n.t('user.warn_unwatch.message', {categories: categories}), + I18n.t('user.warn_unwatch.no_value', {count: changedWatch.remove.length}), I18n.t('user.warn_unwatch.yes_value'), + (yes)=>{ + this.set('unwatchCategoryTopics', yes ? changedWatch.remove : false); + this.send('save'); + }); + this.set("warnedRemoveWatch", true); + return; + } + const userFields = this.get('userFields'); // Update the user fields @@ -148,12 +168,19 @@ export default Ember.Controller.extend(CanCheckEmails, { // Cook the bio for preview model.set('name', this.get('newNameInput')); - return model.save().then(() => { + var options = {}; + if (this.get('warnedRemoveWatch') && this.get('unwatchCategoryTopics')) { + options["unwatchCategoryTopics"] = this.get("unwatchCategoryTopics"); + } + + return model.save(options).then(() => { if (Discourse.User.currentProp('id') === model.get('id')) { Discourse.User.currentProp('name', model.get('name')); } model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw')))); this.set('saved', true); + this.set("unwatchTopics", false); + this.set('warnedRemoveWatch', false); }).catch(popupAjaxError); }, diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 666fc5614..6fabd1b86 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -141,7 +141,7 @@ const User = RestModel.extend({ return Discourse.User.create(this.getProperties(Object.keys(this))); }, - save() { + save(options) { const data = this.getProperties( 'bio_raw', 'website', @@ -177,8 +177,12 @@ const User = RestModel.extend({ data[s] = this.get(`user_option.${s}`); }); + var updatedState = {}; + ['muted','watched','tracked'].forEach(s => { let cats = this.get(s + 'Categories').map(c => c.get('id')); + updatedState[s + '_category_ids'] = cats; + // HACK: denote lack of categories if (cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; @@ -188,6 +192,10 @@ const User = RestModel.extend({ data['edit_history_public'] = this.get('user_option.edit_history_public'); } + if (options && options.unwatchCategoryTopics) { + data.unwatch_category_topics = options.unwatchCategoryTopics; + } + // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); return Discourse.ajax(`/users/${this.get('username_lower')}`, { @@ -197,6 +205,7 @@ const User = RestModel.extend({ this.set('bio_excerpt', result.user.bio_excerpt); const userProps = Em.getProperties(this.get('user_option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); + this.setProperties(updatedState); }).finally(() => { this.set('isSaving', false); }); @@ -352,6 +361,16 @@ const User = RestModel.extend({ this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); }, + changedCategoryNotifications: function(type) { + const ids = this.get(type + "Categories").map(c => c.id); + const oldIds = this.get(type + "_category_ids"); + + return { + add: _.difference(ids, oldIds), + remove: _.difference(oldIds, ids), + } + }, + @computed("can_delete_account", "reply_count", "topic_count") canDeleteAccount(canDeleteAccount, replyCount, topicCount) { return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1; diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 32785d480..8ebc13380 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -89,6 +89,10 @@ class UsersController < ApplicationController user = fetch_user_from_params guardian.ensure_can_edit!(user) + if params[:unwatch_category_topics] + TopicUser.unwatch_categories!(user, params[:unwatch_category_topics]) + end + if params[:user_fields].present? params[:custom_fields] = {} unless params[:custom_fields].present? diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 78480319c..3079385ee 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -59,6 +59,32 @@ class TopicUser < ActiveRecord::Base topic_user.save end + def unwatch_categories!(user, category_ids) + + track_threshold = user.user_option.auto_track_topics_after_msecs + + sql = < :track_threshold AND :track_threshold >= 0 THEN :tracking + ELSE :regular + end + FROM topics t + WHERE t.id = tu.topic_id AND tu.notification_level <> :muted AND category_id IN (:category_ids) AND tu.user_id = :user_id +SQL + + exec_sql(sql, + watching: notification_levels[:watching], + tracking: notification_levels[:tracking], + regular: notification_levels[:regular], + muted: notification_levels[:muted], + category_ids: category_ids, + user_id: user.id, + track_threshold: track_threshold + ) + end + # Find the information specific to a user in a forum topic def lookup_for(user, topics) # If the user isn't logged in, there's no last read posts diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0f83190a4..bee0eafe9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -574,6 +574,13 @@ en: failed_to_move: "Failed to move selected messages (perhaps your network is down)" select_all: "Select All" + warn_unwatch: + message: "Also stop watching previously watched topics in {{categories}}?" + yes_value: "Yes, unwatch topics" + no_value: + one: "No, only unwatch category" + other: "No, only unwatch categories" + change_password: success: "(email sent)" in_progress: "(sending email)" diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index 5b609c0c8..3259316c2 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -2,6 +2,34 @@ require 'rails_helper' describe TopicUser do + describe "#unwatch_categories!" do + it "correctly unwatches categories" do + + op_topic = Fabricate(:topic) + another_topic = Fabricate(:topic) + tracked_topic = Fabricate(:topic) + + user = op_topic.user + watching = TopicUser.notification_levels[:watching] + regular = TopicUser.notification_levels[:regular] + tracking = TopicUser.notification_levels[:tracking] + + TopicUser.change(user.id, op_topic, notification_level: watching) + TopicUser.change(user.id, another_topic, notification_level: watching) + TopicUser.change(user.id, tracked_topic, notification_level: watching, total_msecs_viewed: SiteSetting.default_other_auto_track_topics_after_msecs + 1) + + TopicUser.unwatch_categories!(user, [Fabricate(:category).id, Fabricate(:category).id]) + expect(TopicUser.get(another_topic, user).notification_level).to eq(watching) + + TopicUser.unwatch_categories!(user, [op_topic.category_id]) + + expect(TopicUser.get(op_topic, user).notification_level).to eq(watching) + expect(TopicUser.get(another_topic, user).notification_level).to eq(regular) + expect(TopicUser.get(tracked_topic, user).notification_level).to eq(tracking) + end + + end + describe '#notification_levels' do context "verify enum sequence" do before do