mirror of https://github.com/zulip/zulip.git
settings: Add display setting for demoting inactive streams.
This adds a setting to control Zulip's default behavior of sorting to bottom and graying out inactive streams. The previous logic is still the default "automatic", but this gives users more control. See the models.py comment for details. Fixes #11524.
This commit is contained in:
parent
710fc6767f
commit
5a130097bf
|
@ -607,6 +607,12 @@ var event_fixtures = {
|
|||
setting: true,
|
||||
},
|
||||
|
||||
update_display_settings__demote_inactive_streams: {
|
||||
type: 'update_display_settings',
|
||||
setting_name: 'demote_inactive_streams',
|
||||
setting: 2,
|
||||
},
|
||||
|
||||
update_display_settings__translate_emoticons: {
|
||||
type: 'update_display_settings',
|
||||
setting_name: 'translate_emoticons',
|
||||
|
@ -1389,6 +1395,14 @@ with_overrides(function (override) {
|
|||
page_params.fluid_layout_width = false;
|
||||
dispatch(event);
|
||||
assert_same(page_params.fluid_layout_width, true);
|
||||
|
||||
global.with_stub(function (stub) {
|
||||
event = event_fixtures.update_display_settings__demote_inactive_streams;
|
||||
override('stream_list.update_streams_sidebar', stub.f);
|
||||
page_params.demote_inactive_streams = 1;
|
||||
dispatch(event);
|
||||
assert_same(page_params.demote_inactive_streams, 2);
|
||||
});
|
||||
});
|
||||
|
||||
with_overrides(function (override) {
|
||||
|
|
|
@ -55,6 +55,8 @@ const denmark_stream = {
|
|||
// to make our tests more self-containted.
|
||||
|
||||
zrequire('stream_data');
|
||||
set_global('i18n', global.stub_i18n);
|
||||
zrequire('settings_display');
|
||||
|
||||
run_test('stream_data', () => {
|
||||
assert.equal(stream_data.get_sub_by_name('Denmark'), undefined);
|
||||
|
|
|
@ -9,6 +9,7 @@ set_global('$', function () {
|
|||
|
||||
set_global('blueslip', global.make_zblueslip());
|
||||
set_global('document', null);
|
||||
set_global('i18n', global.stub_i18n);
|
||||
global.stub_out_jquery();
|
||||
|
||||
zrequire('color_data');
|
||||
|
@ -24,6 +25,7 @@ zrequire('Filter', 'js/filter');
|
|||
zrequire('MessageListData', 'js/message_list_data');
|
||||
zrequire('MessageListView', 'js/message_list_view');
|
||||
zrequire('message_list');
|
||||
zrequire('settings_display', 'js/settings_display');
|
||||
|
||||
run_test('basics', () => {
|
||||
var denmark = {
|
||||
|
@ -291,15 +293,45 @@ run_test('is_active', () => {
|
|||
|
||||
var sub;
|
||||
|
||||
page_params.demote_inactive_streams =
|
||||
settings_display.demote_inactive_streams_values.automatic.code;
|
||||
|
||||
sub = {name: 'pets', subscribed: false, stream_id: 111};
|
||||
stream_data.add_sub('pets', sub);
|
||||
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
stream_data.set_filter_out_inactives(true);
|
||||
stream_data.subscribe_myself(sub);
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
stream_data.unsubscribe_myself(sub);
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
sub.pin_to_top = true;
|
||||
assert(stream_data.is_active(sub));
|
||||
sub.pin_to_top = false;
|
||||
|
||||
var opts = {
|
||||
stream_id: 222,
|
||||
message_id: 108,
|
||||
topic_name: 'topic2',
|
||||
};
|
||||
topic_data.add_message(opts);
|
||||
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
page_params.demote_inactive_streams =
|
||||
settings_display.demote_inactive_streams_values.always.code;
|
||||
|
||||
sub = {name: 'pets', subscribed: false, stream_id: 111};
|
||||
stream_data.add_sub('pets', sub);
|
||||
|
||||
assert(!stream_data.is_active(sub));
|
||||
|
||||
sub.pin_to_top = true;
|
||||
assert(stream_data.is_active(sub));
|
||||
sub.pin_to_top = false;
|
||||
|
||||
stream_data.subscribe_myself(sub);
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
|
@ -309,13 +341,29 @@ run_test('is_active', () => {
|
|||
sub = {name: 'lunch', subscribed: false, stream_id: 222};
|
||||
stream_data.add_sub('lunch', sub);
|
||||
|
||||
assert(!stream_data.is_active(sub));
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
topic_data.add_message(opts);
|
||||
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
page_params.demote_inactive_streams =
|
||||
settings_display.demote_inactive_streams_values.never.code;
|
||||
|
||||
sub = {name: 'pets', subscribed: false, stream_id: 111};
|
||||
stream_data.add_sub('pets', sub);
|
||||
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
stream_data.subscribe_myself(sub);
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
stream_data.unsubscribe_myself(sub);
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
sub.pin_to_top = true;
|
||||
assert(stream_data.is_active(sub));
|
||||
|
||||
var opts = {
|
||||
stream_id: 222,
|
||||
message_id: 108,
|
||||
topic_name: 'topic2',
|
||||
};
|
||||
topic_data.add_message(opts);
|
||||
|
||||
assert(stream_data.is_active(sub));
|
||||
|
@ -695,6 +743,7 @@ run_test('initialize', () => {
|
|||
}
|
||||
|
||||
initialize();
|
||||
page_params.demote_inactive_streams = 1;
|
||||
page_params.realm_notifications_stream_id = -1;
|
||||
stream_data.initialize();
|
||||
assert(!stream_data.is_filtering_inactives());
|
||||
|
|
|
@ -18,6 +18,8 @@ zrequire('list_cursor');
|
|||
zrequire('stream_list');
|
||||
zrequire('topic_zoom');
|
||||
zrequire('ui');
|
||||
set_global('i18n', global.stub_i18n);
|
||||
zrequire('settings_display');
|
||||
|
||||
stream_color.initialize();
|
||||
|
||||
|
@ -33,10 +35,15 @@ set_global('keydown_util', {
|
|||
handle: noop,
|
||||
});
|
||||
|
||||
set_global('page_params', {
|
||||
is_admin: false,
|
||||
realm_users: [],
|
||||
});
|
||||
|
||||
run_test('create_sidebar_row', () => {
|
||||
// Make a couple calls to create_sidebar_row() and make sure they
|
||||
// generate the right markup as well as play nice with get_stream_li().
|
||||
|
||||
page_params.demote_inactive_streams = 1;
|
||||
var devel = {
|
||||
name: 'devel',
|
||||
stream_id: 100,
|
||||
|
|
|
@ -121,6 +121,7 @@ zrequire('starred_messages');
|
|||
zrequire('user_status');
|
||||
zrequire('user_status_ui');
|
||||
zrequire('ui_init');
|
||||
zrequire('settings_display');
|
||||
|
||||
set_global('$', global.make_zjquery());
|
||||
|
||||
|
|
|
@ -376,6 +376,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
|
|||
case 'update_display_settings':
|
||||
var user_display_settings = [
|
||||
'default_language',
|
||||
'demote_inactive_streams',
|
||||
'dense_mode',
|
||||
'emojiset',
|
||||
'high_contrast_mode',
|
||||
|
@ -404,6 +405,9 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
|
|||
if (event.setting_name === 'high_contrast_mode') {
|
||||
$("body").toggleClass("high-contrast");
|
||||
}
|
||||
if (event.setting_name === 'demote_inactive_streams') {
|
||||
stream_list.update_streams_sidebar();
|
||||
}
|
||||
if (event.setting_name === 'dense_mode') {
|
||||
$("body").toggleClass("less_dense_mode");
|
||||
$("body").toggleClass("more_dense_mode");
|
||||
|
|
|
@ -130,6 +130,7 @@ exports.build_page = function () {
|
|||
timezones: moment.tz.names(),
|
||||
can_create_new_bots: settings_bots.can_create_new_bots(),
|
||||
settings_label: settings.settings_label,
|
||||
demote_inactive_streams_values: settings_display.demote_inactive_streams_values,
|
||||
});
|
||||
|
||||
$(".settings-box").html(rendered_settings_tab);
|
||||
|
|
|
@ -28,12 +28,29 @@ exports.set_night_mode = function (bool) {
|
|||
change_display_setting(data, '#display-settings-status');
|
||||
};
|
||||
|
||||
exports.demote_inactive_streams_values = {
|
||||
automatic: {
|
||||
code: 1,
|
||||
description: i18n.t("Automatic"),
|
||||
},
|
||||
always: {
|
||||
code: 2,
|
||||
description: i18n.t("Always"),
|
||||
},
|
||||
never: {
|
||||
code: 3,
|
||||
description: i18n.t("Never"),
|
||||
},
|
||||
};
|
||||
|
||||
exports.set_up = function () {
|
||||
meta.loaded = true;
|
||||
$("#display-settings-status").hide();
|
||||
|
||||
$("#user_timezone").val(page_params.timezone);
|
||||
|
||||
$("#demote_inactive_streams").val(page_params.demote_inactive_streams);
|
||||
|
||||
$(".emojiset_choice[value=" + page_params.emojiset + "]").prop("checked", true);
|
||||
|
||||
$("#default_language_modal [data-dismiss]").click(function () {
|
||||
|
@ -83,6 +100,11 @@ exports.set_up = function () {
|
|||
change_display_setting(data, '#display-settings-status');
|
||||
});
|
||||
|
||||
$('#demote_inactive_streams').change(function () {
|
||||
var data = {demote_inactive_streams: this.value};
|
||||
change_display_setting(data, '#display-settings-status');
|
||||
});
|
||||
|
||||
$("#night_mode").change(function () {
|
||||
exports.set_night_mode(this.checked);
|
||||
});
|
||||
|
|
|
@ -20,8 +20,16 @@ exports.clear_subscriptions = function () {
|
|||
|
||||
exports.clear_subscriptions();
|
||||
|
||||
exports.set_filter_out_inactives = function (want_filter) {
|
||||
filter_out_inactives = want_filter || false;
|
||||
exports.set_filter_out_inactives = function () {
|
||||
if (page_params.demote_inactive_streams ===
|
||||
settings_display.demote_inactive_streams_values.automatic.code) {
|
||||
filter_out_inactives = exports.subscribed_subs().length >= 30;
|
||||
} else if (page_params.demote_inactive_streams ===
|
||||
settings_display.demote_inactive_streams_values.always.code) {
|
||||
filter_out_inactives = true;
|
||||
} else {
|
||||
filter_out_inactives = false;
|
||||
}
|
||||
};
|
||||
|
||||
// for testing:
|
||||
|
@ -30,7 +38,8 @@ exports.is_filtering_inactives = function () {
|
|||
};
|
||||
|
||||
exports.is_active = function (sub) {
|
||||
if (!filter_out_inactives) {
|
||||
exports.set_filter_out_inactives();
|
||||
if (!filter_out_inactives || sub.pin_to_top) {
|
||||
// If users don't want to filter inactive streams
|
||||
// to the bottom, we respect that setting and don't
|
||||
// treat any streams as dormant.
|
||||
|
@ -730,8 +739,7 @@ exports.initialize = function () {
|
|||
page_params.notifications_stream = "";
|
||||
}
|
||||
|
||||
var has_many_streams = exports.subscribed_subs().length >= 30;
|
||||
exports.set_filter_out_inactives(has_many_streams);
|
||||
exports.set_filter_out_inactives();
|
||||
|
||||
// Garbage collect data structures that were only used for initialization.
|
||||
delete page_params.subscriptions;
|
||||
|
|
|
@ -56,6 +56,15 @@
|
|||
"setting_name" "fluid_layout_width"
|
||||
"is_checked" page_params.fluid_layout_width
|
||||
"label" settings_label.fluid_layout_width}}
|
||||
|
||||
<div class="input-group">
|
||||
<label for="demote_inactive_streams" class="dropdown-title">{{t "Demote inactive streams" }}:</label>
|
||||
<select name="demote_inactive_streams" id="demote_inactive_streams">
|
||||
{{#each demote_inactive_streams_values}}
|
||||
<option value='{{ this.code }}'>{{ this.description }}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="user-time-settings">
|
||||
|
|
|
@ -3680,7 +3680,7 @@ def do_change_enter_sends(user_profile: UserProfile, enter_sends: bool) -> None:
|
|||
|
||||
def do_set_user_display_setting(user_profile: UserProfile,
|
||||
setting_name: str,
|
||||
setting_value: Union[bool, str]) -> None:
|
||||
setting_value: Union[bool, str, int]) -> None:
|
||||
property_type = UserProfile.property_types[setting_name]
|
||||
assert isinstance(setting_value, property_type)
|
||||
setattr(user_profile, setting_name, setting_value)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-03-08 19:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zerver', '0227_inline_url_embed_preview_default_off'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='demote_inactive_streams',
|
||||
field=models.PositiveSmallIntegerField(default=1),
|
||||
),
|
||||
]
|
|
@ -875,6 +875,20 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
|
|||
starred_message_counts = models.BooleanField(default=False) # type: bool
|
||||
fluid_layout_width = models.BooleanField(default=False) # type: bool
|
||||
|
||||
# UI setting controlling Zulip's behavior of demoting in the sort
|
||||
# order and graying out streams with no recent traffic. The
|
||||
# default behavior, automatic, enables this behavior once a user
|
||||
# is subscribed to 30+ streams in the webapp.
|
||||
DEMOTE_STREAMS_AUTOMATIC = 1
|
||||
DEMOTE_STREAMS_ALWAYS = 2
|
||||
DEMOTE_STREAMS_NEVER = 3
|
||||
DEMOTE_STREAMS_CHOICES = [
|
||||
DEMOTE_STREAMS_AUTOMATIC,
|
||||
DEMOTE_STREAMS_ALWAYS,
|
||||
DEMOTE_STREAMS_NEVER
|
||||
]
|
||||
demote_inactive_streams = models.PositiveSmallIntegerField(default=DEMOTE_STREAMS_AUTOMATIC)
|
||||
|
||||
# A timezone name from the `tzdata` database, as found in pytz.all_timezones.
|
||||
#
|
||||
# The longest existing name is 32 characters long, so max_length=40 seems
|
||||
|
@ -924,6 +938,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
|
|||
# Define the types of the various automatically managed properties
|
||||
property_types = dict(
|
||||
default_language=str,
|
||||
demote_inactive_streams=int,
|
||||
dense_mode=bool,
|
||||
emojiset=str,
|
||||
left_side_userlist=bool,
|
||||
|
|
|
@ -1799,7 +1799,8 @@ class EventsRegisterTest(ZulipTestCase):
|
|||
test_changes = dict(
|
||||
emojiset = [u'apple', u'twitter'],
|
||||
default_language = [u'es', u'de', u'en'],
|
||||
timezone = [u'US/Mountain', u'US/Samoa', u'Pacific/Galapogos', u'']
|
||||
timezone = [u'US/Mountain', u'US/Samoa', u'Pacific/Galapogos', u''],
|
||||
demote_inactive_streams = [2, 3, 1],
|
||||
) # type: Dict[str, Any]
|
||||
|
||||
property_type = UserProfile.property_types[setting_name]
|
||||
|
@ -1807,6 +1808,8 @@ class EventsRegisterTest(ZulipTestCase):
|
|||
validator = check_bool
|
||||
elif property_type is str:
|
||||
validator = check_string
|
||||
elif property_type is int:
|
||||
validator = check_int
|
||||
else:
|
||||
raise AssertionError("Unexpected property type %s" % (property_type,))
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ class HomeTest(ZulipTestCase):
|
|||
"default_language",
|
||||
"default_language_name",
|
||||
"delivery_email",
|
||||
"demote_inactive_streams",
|
||||
"dense_mode",
|
||||
"development_environment",
|
||||
"email",
|
||||
|
|
|
@ -4,7 +4,7 @@ import ujson
|
|||
from django.http import HttpResponse
|
||||
from django.test import override_settings
|
||||
from mock import patch
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from zerver.lib.initial_password import initial_password
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
|
@ -259,6 +259,7 @@ class ChangeSettingsTest(ZulipTestCase):
|
|||
default_language = 'de',
|
||||
emojiset = 'google',
|
||||
timezone = 'US/Mountain',
|
||||
demote_inactive_streams = 2,
|
||||
) # type: Dict[str, Any]
|
||||
|
||||
email = self.example_email('hamlet')
|
||||
|
@ -267,9 +268,13 @@ class ChangeSettingsTest(ZulipTestCase):
|
|||
# Error if a setting in UserProfile.property_types does not have test values
|
||||
if test_value is None:
|
||||
raise AssertionError('No test created for %s' % (setting_name,))
|
||||
invalid_value = 'invalid_' + setting_name
|
||||
|
||||
if setting_name == 'demote_inactive_streams':
|
||||
invalid_value = 4 # type: Union[int, str]
|
||||
else:
|
||||
invalid_value = 'invalid_' + setting_name
|
||||
data = {setting_name: ujson.dumps(test_value)}
|
||||
|
||||
result = self.client_patch("/json/settings/display", data)
|
||||
self.assert_json_success(result)
|
||||
user_profile = self.example_user('hamlet')
|
||||
|
@ -278,11 +283,16 @@ class ChangeSettingsTest(ZulipTestCase):
|
|||
# Test to make sure invalid settings are not accepted
|
||||
# and saved in the db.
|
||||
data = {setting_name: ujson.dumps(invalid_value)}
|
||||
|
||||
result = self.client_patch("/json/settings/display", data)
|
||||
# the json error for multiple word setting names (ex: default_language)
|
||||
# displays as 'Invalid language'. Using setting_name.split('_') to format.
|
||||
self.assert_json_error(result, "Invalid %s '%s'" % (setting_name.split('_')[-1],
|
||||
invalid_value))
|
||||
if setting_name == 'demote_inactive_streams':
|
||||
self.assert_json_error(result, "Invalid setting value '%s'" % (invalid_value,))
|
||||
else:
|
||||
self.assert_json_error(result, "Invalid %s '%s'" % (setting_name.split('_')[-1],
|
||||
invalid_value))
|
||||
|
||||
user_profile = self.example_user('hamlet')
|
||||
self.assertNotEqual(getattr(user_profile, setting_name), invalid_value)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from zerver.lib.send_email import send_email, FromAddress
|
|||
from zerver.lib.i18n import get_available_language_codes
|
||||
from zerver.lib.response import json_success, json_error
|
||||
from zerver.lib.upload import upload_avatar_image
|
||||
from zerver.lib.validator import check_bool, check_string
|
||||
from zerver.lib.validator import check_bool, check_string, check_int
|
||||
from zerver.lib.request import JsonableError
|
||||
from zerver.lib.timezone import get_all_timezones
|
||||
from zerver.models import UserProfile, name_changes_disabled, avatar_changes_disabled
|
||||
|
@ -126,6 +126,7 @@ def update_display_settings_backend(
|
|||
default_language: Optional[bool]=REQ(validator=check_string, default=None),
|
||||
left_side_userlist: Optional[bool]=REQ(validator=check_bool, default=None),
|
||||
emojiset: Optional[str]=REQ(validator=check_string, default=None),
|
||||
demote_inactive_streams: Optional[int]=REQ(validator=check_int, default=None),
|
||||
timezone: Optional[str]=REQ(validator=check_string, default=None)) -> HttpResponse:
|
||||
|
||||
if (default_language is not None and
|
||||
|
@ -140,6 +141,10 @@ def update_display_settings_backend(
|
|||
emojiset not in UserProfile.emojiset_choices()):
|
||||
raise JsonableError(_("Invalid emojiset '%s'") % (emojiset,))
|
||||
|
||||
if (demote_inactive_streams is not None and
|
||||
demote_inactive_streams not in UserProfile.DEMOTE_STREAMS_CHOICES):
|
||||
raise JsonableError(_("Invalid setting value '%s'") % (demote_inactive_streams,))
|
||||
|
||||
request_settings = {k: v for k, v in list(locals().items()) if k in user_profile.property_types}
|
||||
result = {} # type: Dict[str, Any]
|
||||
for k, v in list(request_settings.items()):
|
||||
|
|
Loading…
Reference in New Issue