mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 21:43:21 +00:00
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:
@@ -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);
|
||||
}());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"])}
|
||||
|
||||
19
zerver/migrations/0022_subscription_pin_to_top.py
Normal file
19
zerver/migrations/0022_subscription_pin_to_top.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user