Add ability to pin streams to top of the streams sidebar list.

Based on work by Lauren Long, with some tweaks by tabbott.
This commit is contained in:
Kartik Maji
2016-06-30 22:26:09 -07:00
committed by Tim Abbott
parent a32167d921
commit f8bb7503e6
14 changed files with 247 additions and 12 deletions

View File

@@ -120,3 +120,37 @@ global.use_template('stream_privacy');
assert(li.find('.stream-privacy').find("i").hasClass("icon-vector-lock"));
}());
(function test_sort_pin_to_top_streams() {
var stream_search_box = $('<input class="stream-list-filter" type="text" placeholder="Search streams">');
var stream_filters = $('<ul id="stream_filters">');
$("body").empty();
$("body").append(stream_search_box);
$("body").append(stream_filters);
var develSub = {
name: 'devel',
stream_id: 1000,
color: 'blue',
id: 5,
pin_to_top: false,
subscribed: true,
sidebar_li: stream_list.add_stream_to_sidebar('devel')
};
global.stream_data.add_sub('devel', develSub);
var socialSub = {
name: 'social',
stream_id: 2000,
color: 'green',
id: 6,
pin_to_top: true,
subscribed: true,
sidebar_li: stream_list.add_stream_to_sidebar('social')
};
global.stream_data.add_sub('social', socialSub);
stream_list.build_stream_list();
assert.equal(socialSub.sidebar_li.nextAll().find('[ data-name="devel"]').length, 1);
}());

View File

@@ -559,6 +559,13 @@ exports.register_click_handlers = function () {
e.stopPropagation();
});
$('body').on('click', '.pin_to_top', function (e) {
var stream = $(e.currentTarget).parents('ul').attr('data-name');
popovers.hide_stream_sidebar_popover();
subs.toggle_pin_to_top_stream(stream);
e.stopPropagation();
});
$('body').on('click', '.open_stream_settings', function (e) {
var stream = $(e.currentTarget).parents('ul').attr('data-name');
popovers.hide_stream_sidebar_popover();

View File

@@ -138,6 +138,9 @@ function get_events_success(events) {
});
} else if (event.op === 'update') {
subs.update_subscription_properties(event.name, event.property, event.value);
if (event.property === 'pin_to_top') {
subs.pin_or_unpin_stream(event.name);
}
} else if (event.op === 'peer_add' || event.op === 'peer_remove') {
_.each(event.subscriptions, function (sub) {
var js_event_type;

View File

@@ -8,6 +8,7 @@ var private_messages_open = false;
var last_private_message_count = 0;
var last_mention_count = 0;
var previous_sort_order;
var previous_unpinned_order;
function active_stream_name() {
if (narrow.active()) {
@@ -54,8 +55,33 @@ exports.build_stream_list = function () {
streams = filter_streams_by_search(streams);
var sort_recent = (streams.length > 40);
var pinned_streams = [];
var unpinned_streams = [];
var parent = $('#stream_filters');
var elems = [];
streams.sort(function (a, b) {
function add_sidebar_li(stream) {
var li = $(stream_data.get_sub(stream).sidebar_li);
if (sort_recent) {
if (! stream_data.recent_subjects.has(stream)) {
li.addClass('inactive_stream');
} else {
li.removeClass('inactive_stream');
}
}
elems.push(li.get(0));
}
_.each(streams, function (stream) {
var pinned = stream_data.get_sub(stream).pin_to_top;
if (pinned) {
pinned_streams.push(stream);
} else {
unpinned_streams.push(stream);
}
});
unpinned_streams.sort(function (a, b) {
if (sort_recent) {
if (stream_data.recent_subjects.has(b) && ! stream_data.recent_subjects.has(a)) {
return 1;
@@ -66,18 +92,17 @@ exports.build_stream_list = function () {
return util.strcmp(a, b);
});
if (previous_sort_order !== undefined
&& util.array_compare(previous_sort_order, streams)) {
streams = pinned_streams.concat(unpinned_streams);
if (previous_sort_order !== undefined && util.array_compare(previous_sort_order, streams)
&& util.array_compare(previous_unpinned_order, unpinned_streams)) {
return;
}
previous_sort_order = streams;
var parent = $('#stream_filters');
previous_unpinned_order = unpinned_streams;
parent.empty();
var elems = [];
_.each(streams, function (stream) {
_.each(pinned_streams, function (stream) {
var li = $(stream_data.get_sub(stream).sidebar_li);
if (sort_recent) {
if (! stream_data.recent_subjects.has(stream)) {
@@ -88,6 +113,15 @@ exports.build_stream_list = function () {
}
elems.push(li.get(0));
});
if (pinned_streams.length > 0) {
_.each(pinned_streams, add_sidebar_li);
elems.push($('<hr class="pinned-stream-split">').get(0));
}
if (unpinned_streams.length > 0) {
_.each(unpinned_streams, add_sidebar_li);
}
$(elems).appendTo(parent);
};
@@ -121,6 +155,14 @@ function zoom_in() {
$("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in");
zoomed_stream = active_stream_name();
// Hide stream list titles and pinned stream splitter
$(".stream-filters-label").each(function () {
$(this).hide();
});
$(".pinned-stream-split").each(function () {
$(this).hide();
});
$("#stream_filters li.narrow-filter").each(function () {
var elt = $(this);
@@ -135,6 +177,14 @@ function zoom_in() {
function zoom_out() {
popovers.hide_all();
zoomed_to_topics = false;
// Show stream list titles and pinned stream splitter
$(".stream-filters-label").each(function () {
$(this).show();
});
$(".pinned-stream-split").each(function () {
$(this).show();
});
$("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out");
$("#stream_filters li.narrow-filter").show();
}
@@ -188,7 +238,8 @@ function build_stream_sidebar_row(name) {
uri: narrow.by_stream_uri(name),
not_in_home_view: (stream_data.in_home_view(name) === false),
invite_only: sub.invite_only,
color: stream_data.get_color(name)
color: stream_data.get_color(name),
pin_to_top: sub.pin_to_top
};
args.dark_background = stream_color.get_color_class(args.color);
var list_item = $(templates.render('stream_sidebar_row', args));
@@ -531,6 +582,14 @@ exports.rename_stream = function (sub) {
exports.build_stream_list(); // big hammer
};
exports.refresh_stream_in_sidebar = function (sub) {
// used by subs.pin_or_unpin_stream,
// since pinning/unpinning requires reordering of streams in the sidebar
sub.sidebar_li = build_stream_sidebar_row(sub.name);
exports.build_stream_list();
exports.update_streams_sidebar();
};
$(function () {
$(document).on('narrow_activated.zulip', function (event) {
reset_to_unnarrowed(active_stream_name() === zoomed_stream);
@@ -608,6 +667,7 @@ $(function () {
exports.remove_narrow_filter(stream_name, 'stream');
// We need to make sure we resort if the removed sub gets added again
previous_sort_order = undefined;
previous_unpinned_order = undefined;
});
$('.show-all-streams').on('click', function (e) {

View File

@@ -144,6 +144,11 @@ exports.toggle_home = function (stream_name) {
set_stream_property(stream_name, 'in_home_view', sub.in_home_view);
};
exports.toggle_pin_to_top_stream = function (stream_name) {
var sub = stream_data.get_sub(stream_name);
set_stream_property(stream_name, 'pin_to_top', !sub.pin_to_top);
};
function update_stream_desktop_notifications(sub, value) {
var desktop_notifications_checkbox = $("#subscription_" + sub.stream_id + " #sub_desktop_notifications_setting .sub_setting_control");
desktop_notifications_checkbox.attr('checked', value);
@@ -156,6 +161,12 @@ function update_stream_audible_notifications(sub, value) {
sub.audible_notifications = value;
}
function update_stream_pin(sub, value) {
var pin_checkbox = $('#pinstream-' + sub.stream_id);
pin_checkbox.attr('checked', value);
sub.pin_to_top = value;
}
function update_stream_name(sub, new_name) {
// Rename the stream internally.
var old_name = sub.name;
@@ -198,6 +209,14 @@ function stream_audible_notifications_clicked(e) {
set_stream_property(stream, 'audible_notifications', sub.audible_notifications);
}
function stream_pin_clicked(e) {
var sub_row = $(e.target).closest('.subscription_row');
var stream = sub_row.find('.subscription_name').text();
var sub = stream_data.get_sub(stream);
exports.toggle_pin_to_top_stream(stream);
}
exports.set_color = function (stream_name, color) {
var sub = stream_data.get_sub(stream_name);
stream_color.update_stream_color(sub, stream_name, color, {update_historical: true});
@@ -387,6 +406,23 @@ exports.mark_sub_unsubscribed = function (sub) {
$(document).trigger($.Event('subscription_remove_done.zulip', {sub: sub}));
};
exports.pin_or_unpin_stream = function (stream_name) {
var sub = stream_data.get_sub(stream_name);
if (stream_name === undefined) {
return;
} else {
stream_list.refresh_stream_in_sidebar(sub);
}
};
exports.sub_pinned_or_unpinned = function (stream_name) {
var sub = stream_data.get_sub(stream_name);
if (stream_name === undefined) {
return;
}
return sub.pin_to_top;
};
exports.receives_desktop_notifications = function (stream_name) {
var sub = stream_data.get_sub(stream_name);
if (sub === undefined) {
@@ -414,6 +450,7 @@ function populate_subscriptions(subs, subscribed) {
invite_only: elem.invite_only,
desktop_notifications: elem.desktop_notifications,
audible_notifications: elem.audible_notifications,
pin_to_top: elem.pin_to_top,
subscribed: subscribed,
email_address: elem.email_address,
stream_id: elem.stream_id,
@@ -544,6 +581,9 @@ exports.update_subscription_properties = function (stream_name, property, value)
case 'email_address':
sub.email_address = value;
break;
case 'pin_to_top':
update_stream_pin(sub, value);
break;
default:
blueslip.warn("Unexpected subscription property type", {property: property,
value: value});
@@ -874,6 +914,8 @@ $(function () {
stream_desktop_notifications_clicked);
$("#subscriptions_table").on("click", "#sub_audible_notifications_setting",
stream_audible_notifications_clicked);
$("#subscriptions_table").on("click", "#sub_pin_setting",
stream_pin_clicked);
$("#subscriptions_table").on("submit", ".subscriber_list_add form", function (e) {
e.preventDefault();

View File

@@ -510,6 +510,11 @@ ul.filters {
margin-right: 5px;
}
.stream-pin-icon {
margin-right: 4px !important;
margin-left: 3px;
}
#global_filters .global-filter {
position: relative;
padding: 1px 10px;

View File

@@ -17,6 +17,16 @@
{{/if}}
</a>
</li>
<li>
<a class="pin_to_top">
<i class="icon-vector-pushpin stream-pin-icon"></i>
{{#if stream.pin_to_top}}
{{#tr this}}Unpin stream <b>__stream.name__</b> from top{{/tr}}
{{else}}
{{#tr this}}Pin stream <b>__stream.name__</b> to top{{/tr}}
{{/if}}
</a>
</li>
<li>
<a class="compose_to_stream">
<i class="icon-vector-edit"></i>

View File

@@ -50,6 +50,12 @@
</div>
</li>
<li>
<div id="sub_pin_setting" class="sub_setting_checkbox">
<input id="pinstream-{{stream_id}}" class="sub_setting_control" type="checkbox" tabindex="-1" {{#if pin_to_top}}checked{{/if}} />
<label class="subscription-control-label">{{t "Pin stream to top<br /> of left sidebar" }}</label>
</div>
</li>
<li>
<span class="sub_setting_control">
<input stream_name="{{name}}" class="colorpicker" id="streamcolor" type="text" value="{{color}}" tabindex="-1" />

View File

@@ -1253,6 +1253,7 @@ def notify_subscriptions_added(user_profile, sub_pairs, stream_emails, no_log=Fa
desktop_notifications=subscription.desktop_notifications,
audible_notifications=subscription.audible_notifications,
description=stream.description,
pin_to_top=subscription.pin_to_top,
subscribers=stream_emails(stream))
for (subscription, stream) in sub_pairs]
event = dict(type="subscription", op="add",
@@ -2544,7 +2545,7 @@ def gather_subscriptions_helper(user_profile):
user_profile = user_profile,
recipient__type = Recipient.STREAM).values(
"recipient__type_id", "in_home_view", "color", "desktop_notifications",
"audible_notifications", "active")
"audible_notifications", "active", "pin_to_top")
stream_ids = [sub["recipient__type_id"] for sub in sub_dicts]
@@ -2583,6 +2584,7 @@ def gather_subscriptions_helper(user_profile):
'color': sub["color"],
'desktop_notifications': sub["desktop_notifications"],
'audible_notifications': sub["audible_notifications"],
'pin_to_top': sub["pin_to_top"],
'stream_id': stream["id"],
'description': stream["description"],
'email_address': encode_email_address_helper(stream["name"], stream["email_token"])}

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('zerver', '0021_migrate_attachment_data'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='pin_to_top',
field=models.BooleanField(default=False),
),
]

View File

@@ -1232,6 +1232,7 @@ class Subscription(ModelReprMixin, models.Model):
DEFAULT_STREAM_COLOR = "#c2c2c2"
color = models.CharField(max_length=10, default=DEFAULT_STREAM_COLOR) # type: text_type
pin_to_top = models.BooleanField(default=False) # type: bool
desktop_notifications = models.BooleanField(default=True) # type: bool
audible_notifications = models.BooleanField(default=True) # type: bool

View File

@@ -23,6 +23,7 @@ from zerver.lib.actions import (
do_change_full_name,
do_change_is_admin,
do_change_stream_description,
do_change_subscription_property,
do_create_user,
do_deactivate_user,
do_regenerate_api_key,
@@ -43,6 +44,7 @@ from zerver.lib.actions import (
do_change_twenty_four_hour_time,
do_change_left_side_userlist,
fetch_initial_state_data,
get_subscription
)
from zerver.lib.event_queue import allocate_client_descriptor
@@ -488,6 +490,22 @@ class EventsRegisterTest(AuthedTestCase):
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_change_pin_stream(self):
# type: () -> None
schema_checker = check_dict([
('type', equals('subscription')),
('op', equals('update')),
('property', equals('pin_to_top')),
('value', check_bool),
])
stream = "Denmark"
sub = get_subscription(stream, self.user_profile)
# The first False is probably a noop, then we get transitions in both directions.
for pinned in (False, True, False):
events = self.do_test(lambda: do_change_subscription_property(self.user_profile, sub, stream, "pin_to_top", pinned))
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_change_is_admin(self):
# type: () -> None
schema_checker = check_dict([

View File

@@ -26,7 +26,7 @@ from zerver.lib.actions import (
create_stream_if_needed, do_add_default_stream, do_add_subscription, do_change_is_admin,
do_create_realm, do_remove_default_stream, do_set_realm_create_stream_by_admins_only,
gather_subscriptions, get_default_streams_for_realm, get_realm, get_stream,
get_user_profile_by_email, set_default_streams,
get_user_profile_by_email, set_default_streams, get_subscription
)
from django.http import HttpResponse
@@ -591,6 +591,33 @@ class SubscriptionPropertiesTest(AuthedTestCase):
self.assert_json_error(
result, "value key is missing from subscription_data[0]")
def test_set_pin_to_top(self):
# type: () -> None
"""
A POST request to /json/subscriptions/property with stream_name and
pin_to_top data pins the stream.
"""
test_email = "hamlet@zulip.com"
self.login(test_email)
user_profile = get_user_profile_by_email(test_email)
old_subs, _ = gather_subscriptions(user_profile)
sub = old_subs[0]
stream_name = sub['name']
new_pin_to_top = not sub['pin_to_top']
result = self.client.post(
"/json/subscriptions/property",
{"subscription_data": ujson.dumps([{"property": "pin_to_top",
"stream": stream_name,
"value": new_pin_to_top}])})
self.assert_json_success(result)
updated_sub = get_subscription(stream_name, user_profile)
self.assertIsNotNone(updated_sub)
self.assertEqual(updated_sub.pin_to_top, new_pin_to_top)
def test_set_invalid_property(self):
# type: () -> None
"""

View File

@@ -488,7 +488,8 @@ def json_subscription_property(request, user_profile, subscription_data=REQ(
property_converters = {"color": check_string, "in_home_view": check_bool,
"desktop_notifications": check_bool,
"audible_notifications": check_bool}
"audible_notifications": check_bool,
"pin_to_top": check_bool}
response_data = []
for change in subscription_data: