mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +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"));
 | 
					    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();
 | 
					        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) {
 | 
					    $('body').on('click', '.open_stream_settings', function (e) {
 | 
				
			||||||
        var stream = $(e.currentTarget).parents('ul').attr('data-name');
 | 
					        var stream = $(e.currentTarget).parents('ul').attr('data-name');
 | 
				
			||||||
        popovers.hide_stream_sidebar_popover();
 | 
					        popovers.hide_stream_sidebar_popover();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,6 +138,9 @@ function get_events_success(events) {
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
            } else if (event.op === 'update') {
 | 
					            } else if (event.op === 'update') {
 | 
				
			||||||
                subs.update_subscription_properties(event.name, event.property, event.value);
 | 
					                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') {
 | 
					            } else if (event.op === 'peer_add' || event.op === 'peer_remove') {
 | 
				
			||||||
                _.each(event.subscriptions, function (sub) {
 | 
					                _.each(event.subscriptions, function (sub) {
 | 
				
			||||||
                    var js_event_type;
 | 
					                    var js_event_type;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ var private_messages_open = false;
 | 
				
			|||||||
var last_private_message_count = 0;
 | 
					var last_private_message_count = 0;
 | 
				
			||||||
var last_mention_count = 0;
 | 
					var last_mention_count = 0;
 | 
				
			||||||
var previous_sort_order;
 | 
					var previous_sort_order;
 | 
				
			||||||
 | 
					var previous_unpinned_order;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function active_stream_name() {
 | 
					function active_stream_name() {
 | 
				
			||||||
    if (narrow.active()) {
 | 
					    if (narrow.active()) {
 | 
				
			||||||
@@ -54,8 +55,33 @@ exports.build_stream_list = function () {
 | 
				
			|||||||
    streams = filter_streams_by_search(streams);
 | 
					    streams = filter_streams_by_search(streams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var sort_recent = (streams.length > 40);
 | 
					    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 (sort_recent) {
 | 
				
			||||||
            if (stream_data.recent_subjects.has(b) && ! stream_data.recent_subjects.has(a)) {
 | 
					            if (stream_data.recent_subjects.has(b) && ! stream_data.recent_subjects.has(a)) {
 | 
				
			||||||
                return 1;
 | 
					                return 1;
 | 
				
			||||||
@@ -66,18 +92,17 @@ exports.build_stream_list = function () {
 | 
				
			|||||||
        return util.strcmp(a, b);
 | 
					        return util.strcmp(a, b);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (previous_sort_order !== undefined
 | 
					    streams = pinned_streams.concat(unpinned_streams);
 | 
				
			||||||
        && util.array_compare(previous_sort_order, streams)) {
 | 
					
 | 
				
			||||||
 | 
					    if (previous_sort_order !== undefined && util.array_compare(previous_sort_order, streams)
 | 
				
			||||||
 | 
					                                          && util.array_compare(previous_unpinned_order, unpinned_streams)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    previous_sort_order = streams;
 | 
					    previous_sort_order = streams;
 | 
				
			||||||
 | 
					    previous_unpinned_order = unpinned_streams;
 | 
				
			||||||
    var parent = $('#stream_filters');
 | 
					 | 
				
			||||||
    parent.empty();
 | 
					    parent.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var elems = [];
 | 
					    _.each(pinned_streams, function (stream) {
 | 
				
			||||||
    _.each(streams, function (stream) {
 | 
					 | 
				
			||||||
        var li = $(stream_data.get_sub(stream).sidebar_li);
 | 
					        var li = $(stream_data.get_sub(stream).sidebar_li);
 | 
				
			||||||
        if (sort_recent) {
 | 
					        if (sort_recent) {
 | 
				
			||||||
            if (! stream_data.recent_subjects.has(stream)) {
 | 
					            if (! stream_data.recent_subjects.has(stream)) {
 | 
				
			||||||
@@ -88,6 +113,15 @@ exports.build_stream_list = function () {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        elems.push(li.get(0));
 | 
					        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);
 | 
					    $(elems).appendTo(parent);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,6 +155,14 @@ function zoom_in() {
 | 
				
			|||||||
    $("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in");
 | 
					    $("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in");
 | 
				
			||||||
    zoomed_stream = active_stream_name();
 | 
					    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 () {
 | 
					    $("#stream_filters li.narrow-filter").each(function () {
 | 
				
			||||||
        var elt = $(this);
 | 
					        var elt = $(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,6 +177,14 @@ function zoom_in() {
 | 
				
			|||||||
function zoom_out() {
 | 
					function zoom_out() {
 | 
				
			||||||
    popovers.hide_all();
 | 
					    popovers.hide_all();
 | 
				
			||||||
    zoomed_to_topics = false;
 | 
					    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");
 | 
					    $("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out");
 | 
				
			||||||
    $("#stream_filters li.narrow-filter").show();
 | 
					    $("#stream_filters li.narrow-filter").show();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -188,7 +238,8 @@ function build_stream_sidebar_row(name) {
 | 
				
			|||||||
                uri: narrow.by_stream_uri(name),
 | 
					                uri: narrow.by_stream_uri(name),
 | 
				
			||||||
                not_in_home_view: (stream_data.in_home_view(name) === false),
 | 
					                not_in_home_view: (stream_data.in_home_view(name) === false),
 | 
				
			||||||
                invite_only: sub.invite_only,
 | 
					                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);
 | 
					    args.dark_background = stream_color.get_color_class(args.color);
 | 
				
			||||||
    var list_item = $(templates.render('stream_sidebar_row', args));
 | 
					    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.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 () {
 | 
					$(function () {
 | 
				
			||||||
    $(document).on('narrow_activated.zulip', function (event) {
 | 
					    $(document).on('narrow_activated.zulip', function (event) {
 | 
				
			||||||
        reset_to_unnarrowed(active_stream_name() === zoomed_stream);
 | 
					        reset_to_unnarrowed(active_stream_name() === zoomed_stream);
 | 
				
			||||||
@@ -608,6 +667,7 @@ $(function () {
 | 
				
			|||||||
        exports.remove_narrow_filter(stream_name, 'stream');
 | 
					        exports.remove_narrow_filter(stream_name, 'stream');
 | 
				
			||||||
        // We need to make sure we resort if the removed sub gets added again
 | 
					        // We need to make sure we resort if the removed sub gets added again
 | 
				
			||||||
        previous_sort_order = undefined;
 | 
					        previous_sort_order = undefined;
 | 
				
			||||||
 | 
					        previous_unpinned_order = undefined;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('.show-all-streams').on('click', function (e) {
 | 
					    $('.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);
 | 
					    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) {
 | 
					function update_stream_desktop_notifications(sub, value) {
 | 
				
			||||||
    var desktop_notifications_checkbox = $("#subscription_" + sub.stream_id + " #sub_desktop_notifications_setting .sub_setting_control");
 | 
					    var desktop_notifications_checkbox = $("#subscription_" + sub.stream_id + " #sub_desktop_notifications_setting .sub_setting_control");
 | 
				
			||||||
    desktop_notifications_checkbox.attr('checked', value);
 | 
					    desktop_notifications_checkbox.attr('checked', value);
 | 
				
			||||||
@@ -156,6 +161,12 @@ function update_stream_audible_notifications(sub, value) {
 | 
				
			|||||||
    sub.audible_notifications = 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) {
 | 
					function update_stream_name(sub, new_name) {
 | 
				
			||||||
    // Rename the stream internally.
 | 
					    // Rename the stream internally.
 | 
				
			||||||
    var old_name = sub.name;
 | 
					    var old_name = sub.name;
 | 
				
			||||||
@@ -198,6 +209,14 @@ function stream_audible_notifications_clicked(e) {
 | 
				
			|||||||
    set_stream_property(stream, 'audible_notifications', sub.audible_notifications);
 | 
					    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) {
 | 
					exports.set_color = function (stream_name, color) {
 | 
				
			||||||
    var sub = stream_data.get_sub(stream_name);
 | 
					    var sub = stream_data.get_sub(stream_name);
 | 
				
			||||||
    stream_color.update_stream_color(sub, stream_name, color, {update_historical: true});
 | 
					    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}));
 | 
					    $(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) {
 | 
					exports.receives_desktop_notifications = function (stream_name) {
 | 
				
			||||||
    var sub = stream_data.get_sub(stream_name);
 | 
					    var sub = stream_data.get_sub(stream_name);
 | 
				
			||||||
    if (sub === undefined) {
 | 
					    if (sub === undefined) {
 | 
				
			||||||
@@ -414,6 +450,7 @@ function populate_subscriptions(subs, subscribed) {
 | 
				
			|||||||
                                           invite_only: elem.invite_only,
 | 
					                                           invite_only: elem.invite_only,
 | 
				
			||||||
                                           desktop_notifications: elem.desktop_notifications,
 | 
					                                           desktop_notifications: elem.desktop_notifications,
 | 
				
			||||||
                                           audible_notifications: elem.audible_notifications,
 | 
					                                           audible_notifications: elem.audible_notifications,
 | 
				
			||||||
 | 
					                                           pin_to_top: elem.pin_to_top,
 | 
				
			||||||
                                           subscribed: subscribed,
 | 
					                                           subscribed: subscribed,
 | 
				
			||||||
                                           email_address: elem.email_address,
 | 
					                                           email_address: elem.email_address,
 | 
				
			||||||
                                           stream_id: elem.stream_id,
 | 
					                                           stream_id: elem.stream_id,
 | 
				
			||||||
@@ -544,6 +581,9 @@ exports.update_subscription_properties = function (stream_name, property, value)
 | 
				
			|||||||
    case 'email_address':
 | 
					    case 'email_address':
 | 
				
			||||||
        sub.email_address = value;
 | 
					        sub.email_address = value;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					    case 'pin_to_top':
 | 
				
			||||||
 | 
					        update_stream_pin(sub, value);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
        blueslip.warn("Unexpected subscription property type", {property: property,
 | 
					        blueslip.warn("Unexpected subscription property type", {property: property,
 | 
				
			||||||
                                                                value: value});
 | 
					                                                                value: value});
 | 
				
			||||||
@@ -874,6 +914,8 @@ $(function () {
 | 
				
			|||||||
                                 stream_desktop_notifications_clicked);
 | 
					                                 stream_desktop_notifications_clicked);
 | 
				
			||||||
    $("#subscriptions_table").on("click", "#sub_audible_notifications_setting",
 | 
					    $("#subscriptions_table").on("click", "#sub_audible_notifications_setting",
 | 
				
			||||||
                                 stream_audible_notifications_clicked);
 | 
					                                 stream_audible_notifications_clicked);
 | 
				
			||||||
 | 
					    $("#subscriptions_table").on("click", "#sub_pin_setting",
 | 
				
			||||||
 | 
					                                 stream_pin_clicked);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#subscriptions_table").on("submit", ".subscriber_list_add form", function (e) {
 | 
					    $("#subscriptions_table").on("submit", ".subscriber_list_add form", function (e) {
 | 
				
			||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -510,6 +510,11 @@ ul.filters {
 | 
				
			|||||||
    margin-right: 5px;
 | 
					    margin-right: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stream-pin-icon {
 | 
				
			||||||
 | 
					    margin-right: 4px !important;
 | 
				
			||||||
 | 
					    margin-left: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#global_filters .global-filter {
 | 
					#global_filters .global-filter {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
    padding: 1px 10px;
 | 
					    padding: 1px 10px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,16 @@
 | 
				
			|||||||
      {{/if}}
 | 
					      {{/if}}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </li>
 | 
					  </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>
 | 
					  <li>
 | 
				
			||||||
    <a class="compose_to_stream">
 | 
					    <a class="compose_to_stream">
 | 
				
			||||||
      <i class="icon-vector-edit"></i>
 | 
					      <i class="icon-vector-edit"></i>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,6 +50,12 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          </li>
 | 
					          </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>
 | 
					          <li>
 | 
				
			||||||
            <span class="sub_setting_control">
 | 
					            <span class="sub_setting_control">
 | 
				
			||||||
              <input stream_name="{{name}}" class="colorpicker" id="streamcolor" type="text" value="{{color}}" tabindex="-1" />
 | 
					              <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,
 | 
					                    desktop_notifications=subscription.desktop_notifications,
 | 
				
			||||||
                    audible_notifications=subscription.audible_notifications,
 | 
					                    audible_notifications=subscription.audible_notifications,
 | 
				
			||||||
                    description=stream.description,
 | 
					                    description=stream.description,
 | 
				
			||||||
 | 
					                    pin_to_top=subscription.pin_to_top,
 | 
				
			||||||
                    subscribers=stream_emails(stream))
 | 
					                    subscribers=stream_emails(stream))
 | 
				
			||||||
            for (subscription, stream) in sub_pairs]
 | 
					            for (subscription, stream) in sub_pairs]
 | 
				
			||||||
    event = dict(type="subscription", op="add",
 | 
					    event = dict(type="subscription", op="add",
 | 
				
			||||||
@@ -2544,7 +2545,7 @@ def gather_subscriptions_helper(user_profile):
 | 
				
			|||||||
        user_profile    = user_profile,
 | 
					        user_profile    = user_profile,
 | 
				
			||||||
        recipient__type = Recipient.STREAM).values(
 | 
					        recipient__type = Recipient.STREAM).values(
 | 
				
			||||||
        "recipient__type_id", "in_home_view", "color", "desktop_notifications",
 | 
					        "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]
 | 
					    stream_ids = [sub["recipient__type_id"] for sub in sub_dicts]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2583,6 +2584,7 @@ def gather_subscriptions_helper(user_profile):
 | 
				
			|||||||
                       'color': sub["color"],
 | 
					                       'color': sub["color"],
 | 
				
			||||||
                       'desktop_notifications': sub["desktop_notifications"],
 | 
					                       'desktop_notifications': sub["desktop_notifications"],
 | 
				
			||||||
                       'audible_notifications': sub["audible_notifications"],
 | 
					                       'audible_notifications': sub["audible_notifications"],
 | 
				
			||||||
 | 
					                       'pin_to_top': sub["pin_to_top"],
 | 
				
			||||||
                       'stream_id': stream["id"],
 | 
					                       'stream_id': stream["id"],
 | 
				
			||||||
                       'description': stream["description"],
 | 
					                       'description': stream["description"],
 | 
				
			||||||
                       'email_address': encode_email_address_helper(stream["name"], stream["email_token"])}
 | 
					                       '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"
 | 
					    DEFAULT_STREAM_COLOR = "#c2c2c2"
 | 
				
			||||||
    color = models.CharField(max_length=10, default=DEFAULT_STREAM_COLOR) # type: text_type
 | 
					    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
 | 
					    desktop_notifications = models.BooleanField(default=True) # type: bool
 | 
				
			||||||
    audible_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_full_name,
 | 
				
			||||||
    do_change_is_admin,
 | 
					    do_change_is_admin,
 | 
				
			||||||
    do_change_stream_description,
 | 
					    do_change_stream_description,
 | 
				
			||||||
 | 
					    do_change_subscription_property,
 | 
				
			||||||
    do_create_user,
 | 
					    do_create_user,
 | 
				
			||||||
    do_deactivate_user,
 | 
					    do_deactivate_user,
 | 
				
			||||||
    do_regenerate_api_key,
 | 
					    do_regenerate_api_key,
 | 
				
			||||||
@@ -43,6 +44,7 @@ from zerver.lib.actions import (
 | 
				
			|||||||
    do_change_twenty_four_hour_time,
 | 
					    do_change_twenty_four_hour_time,
 | 
				
			||||||
    do_change_left_side_userlist,
 | 
					    do_change_left_side_userlist,
 | 
				
			||||||
    fetch_initial_state_data,
 | 
					    fetch_initial_state_data,
 | 
				
			||||||
 | 
					    get_subscription
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zerver.lib.event_queue import allocate_client_descriptor
 | 
					from zerver.lib.event_queue import allocate_client_descriptor
 | 
				
			||||||
@@ -488,6 +490,22 @@ class EventsRegisterTest(AuthedTestCase):
 | 
				
			|||||||
            error = schema_checker('events[0]', events[0])
 | 
					            error = schema_checker('events[0]', events[0])
 | 
				
			||||||
            self.assert_on_error(error)
 | 
					            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):
 | 
					    def test_change_is_admin(self):
 | 
				
			||||||
        # type: () -> None
 | 
					        # type: () -> None
 | 
				
			||||||
        schema_checker = check_dict([
 | 
					        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,
 | 
					    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,
 | 
					    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,
 | 
					    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
 | 
					from django.http import HttpResponse
 | 
				
			||||||
@@ -591,6 +591,33 @@ class SubscriptionPropertiesTest(AuthedTestCase):
 | 
				
			|||||||
        self.assert_json_error(
 | 
					        self.assert_json_error(
 | 
				
			||||||
            result, "value key is missing from subscription_data[0]")
 | 
					            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):
 | 
					    def test_set_invalid_property(self):
 | 
				
			||||||
        # type: () -> None
 | 
					        # 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,
 | 
					    property_converters = {"color": check_string, "in_home_view": check_bool,
 | 
				
			||||||
                           "desktop_notifications": check_bool,
 | 
					                           "desktop_notifications": check_bool,
 | 
				
			||||||
                           "audible_notifications": check_bool}
 | 
					                           "audible_notifications": check_bool,
 | 
				
			||||||
 | 
					                           "pin_to_top": check_bool}
 | 
				
			||||||
    response_data = []
 | 
					    response_data = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for change in subscription_data:
 | 
					    for change in subscription_data:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user