diff --git a/tools/jslint/check-all.js b/tools/jslint/check-all.js index 1333d27b72..1a76ad0c88 100644 --- a/tools/jslint/check-all.js +++ b/tools/jslint/check-all.js @@ -42,7 +42,7 @@ var globals = + ' maybe_scroll_to_selected recenter_pointer_on_display suppress_scroll_pointer_update' + ' process_visible_unread_messages message_range message_in_table process_loaded_for_unread' + ' mark_all_as_read message_unread process_read_messages unread_in_current_view' - + ' fast_forward_pointer' + + ' fast_forward_pointer recent_subjects' ; diff --git a/zephyr/static/js/narrow.js b/zephyr/static/js/narrow.js index dcaacde7d7..1b571d3bb1 100644 --- a/zephyr/static/js/narrow.js +++ b/zephyr/static/js/narrow.js @@ -371,15 +371,32 @@ exports.activate = function (operators, opts) { compose.update_recipient_on_narrow(); compose.update_faded_messages(); - $("ul.filters li").removeClass('active-filter'); + $("ul.filters li").removeClass('active-filter active-subject-filter'); + $("ul.expanded_subjects").addClass('hidden'); + + function expand_stream(stream) { + var filter_li = ui.get_filter_li('stream', operators[0][1]); + $('ul.expanded_subjects', filter_li).removeClass('hidden'); + + return filter_li; + } + if (operators.length === 1) { if (operators[0][0] === 'in' && operators[0][1] === 'all') { $("#global_filters li[data-name='all']").addClass('active-filter'); } else if (operators[0][0] === "stream") { - ui.get_filter_li('stream', operators[0][1]).addClass('active-filter'); + var filter_li = expand_stream(operators[0][0]); + filter_li.addClass('active-filter'); } else if (operators[0][0] === "is" && operators[0][1] === "private-message") { $("#global_filters li[data-name='private']").addClass('active-filter'); } + } else if (operators.length === 2) { + if (operators[0][0] === 'stream' && + operators[1][0] === 'subject') { + expand_stream(operators[0][0]); + ui.get_subject_filter_li(operators[0][1], operators[1][1]) + .addClass('active-subject-filter'); + } } }; @@ -448,7 +465,8 @@ exports.deactivate = function () { hashchange.save_narrow(); - $("ul.filters li").removeClass('active-filter'); + $("ul.filters li").removeClass('active-filter active-subject-filter'); + $("ul.expanded_subjects").addClass('hidden'); $("#global_filters li[data-name='home']").addClass('active-filter'); // This really shouldn't be necessary since the act of unnarrowing diff --git a/zephyr/static/js/ui.js b/zephyr/static/js/ui.js index a43d7f14b0..006487542f 100644 --- a/zephyr/static/js/ui.js +++ b/zephyr/static/js/ui.js @@ -1014,20 +1014,33 @@ $(function () { e.preventDefault(); }); - $('#stream_filters li').on('click', 'a', function (e) { + $('#stream_filters li').on('click', 'a.subscription_name', function (e) { var stream = $(e.target).parents('li').data('name'); narrow.by('stream', stream, {select_first_unread: true}); e.preventDefault(); }); - $('#stream_filters li').on('click', 'a', function (e) { - var stream = $(e.target).parents('li').data('name'); - narrow.by('stream', stream, {select_first_unread: true}); + $('#stream_filters').on('click', '.expanded_subject a', function (e) { + var stream = $(e.target).parents('ul').data('stream'); + var subject = $(e.target).parents('li').data('name'); + + narrow.activate([['stream', stream], + ['subject', subject]], + {select_first_unread: true}); e.preventDefault(); }); + $('#stream_filters').on('click', '.streamlist_expand', function (e) { + var stream_li = $(e.target).parents('li'); + var stream = stream_li.data('name'); + + $('ul.expanded_subjects', stream_li).toggleClass('hidden'); + + return false; + }); + $('.composebox-close').click(function (e) { compose.cancel(); }); $('.compose_stream_button').click(function (e) { compose.set_mode('stream'); @@ -1112,7 +1125,7 @@ $(function () { }); function sort_narrow_list() { - var items = $('#stream_filters li').get(); + var items = $('#stream_filters > li').get(); var parent = $('#stream_filters'); items.sort(function(a,b){ return $(a).attr('data-name').localeCompare($(b).attr('data-name')); @@ -1125,16 +1138,25 @@ function sort_narrow_list() { }); } -exports.get_filter_li = function(type, name) { +function iterate_to_find(selector, data_name, context) { var retval = $(); - $("#" + type + "_filters li").each(function (idx, elem) { + $(selector, context).each(function (idx, elem) { var jelem = $(elem); - if (jelem.attr('data-name') === name) { + if (jelem.attr('data-name') === data_name) { retval = jelem; return false; } }); return retval; +} + +exports.get_filter_li = function(type, name) { + return iterate_to_find("#" + type + "_filters > li", name); +}; + +exports.get_subject_filter_li = function(stream, subject) { + var stream_li = exports.get_filter_li('stream', stream); + return iterate_to_find(".expanded_subjects li", subject, stream_li); }; exports.add_narrow_filter = function(name, type, uri) { @@ -1289,5 +1311,58 @@ exports.restore_compose_cursor = function () { .caret(saved_compose_cursor, saved_compose_cursor); }; +exports.update_recent_subjects = function () { + function same(arr1, arr2) { + var i = 0; + + if (arr1.length !== arr2.length) return false; + for (i = 0; i < arr1.length; i++) { + if (arr2[i] !== arr1[i]) { + return false; + } + } + return true; + } + + $("#stream_filters > li").each(function (idx, elem) { + var stream = $(elem).data('name'); + var expander = $('.streamlist_expand', elem); + var subjects = recent_subjects[stream] || []; + var subject_names = $.map(subjects, function (elem, idx) { + return elem.subject; + }); + + expander.toggleClass('hidden', subjects.length === 0); + + var currently_shown = $('ul.expanded_subjects li', elem).map(function(idx, elem) { + return $(elem).text().trim(); + }); + + if (!same(currently_shown, subject_names)) { + var subject_list = $("ul.expanded_subjects", elem); + + var was_hidden = subject_list.length === 0 || subject_list.hasClass('hidden'); + // If this is the first subject in current narrow, show it regardless + var operators = narrow.operators(); + if (subject_list.length === 0 && operators.length > 0 && operators[0][0] === 'stream') { + was_hidden = operators[0][1] !== stream; + } + var active_subject = $("ul.expanded_subjects li.active-subject-filter").text().trim(); + + subject_list.remove(); + + if (subjects.length > 0) { + $(elem).append(templates.render('sidebar_subject_list', + {subjects: subjects, + stream: stream, + hidden: was_hidden})); + if (active_subject !== '') { + exports.get_subject_filter_li(stream, active_subject).addClass('active-subject-filter'); + } + } + } + }); +}; + return exports; }()); diff --git a/zephyr/static/js/zephyr.js b/zephyr/static/js/zephyr.js index 02c64479ea..3add2c46fe 100644 --- a/zephyr/static/js/zephyr.js +++ b/zephyr/static/js/zephyr.js @@ -4,6 +4,7 @@ var narrowed_msg_list; var current_msg_list = home_msg_list; var subject_dict = {}; var people_dict = {}; +var recent_subjects = {}; var queued_mark_as_read = []; var queued_flag_timer; @@ -462,6 +463,40 @@ function case_insensitive_find(term, array) { }).length !== 0; } +var update_recent_subjects = $.debounce(100, ui.update_recent_subjects); + +function process_message_for_recent_subjects(message) { + var current_timestamp = 0; + + if (! recent_subjects.hasOwnProperty(message.display_recipient)) { + recent_subjects[message.display_recipient] = []; + } else { + recent_subjects[message.display_recipient] = + $.grep(recent_subjects[message.display_recipient], function (item) { + if (item.subject === message.subject) { + current_timestamp = item.timestamp; + } + + return item.subject !== message.subject; + }); + } + + var recents = recent_subjects[message.display_recipient]; + if (recents.length >= 5 && message.timestamp > recents[4].timestamp) { + recents = recents.slice(0, recents.length - 1); + } + + recents.push({subject: message.subject, + timestamp: Math.max(message.timestamp, current_timestamp)}); + + recents.sort(function (a, b) { + return a.timestamp < b.timestamp; + }); + + recent_subjects[message.display_recipient] = recents; + update_recent_subjects(); +} + function add_message_metadata(message, dummy) { if (all_msg_list.get(message.id)) { return all_msg_list.get(message.id); @@ -487,6 +522,8 @@ function add_message_metadata(message, dummy) { } message.reply_to = message.sender_email; + process_message_for_recent_subjects(message); + involved_people = [{'full_name': message.sender_full_name, 'email': message.sender_email}]; break; diff --git a/zephyr/static/styles/zephyr.css b/zephyr/static/styles/zephyr.css index c826684410..95500a7288 100644 --- a/zephyr/static/styles/zephyr.css +++ b/zephyr/static/styles/zephyr.css @@ -206,7 +206,7 @@ ul.filters hr { margin-bottom: 10px; } -li.active-filter { +li.active-filter, li.active-subject-filter { font-weight: bold; } @@ -1045,6 +1045,15 @@ table.floating_recipient { vertical-align: middle; } +ul.expanded_subjects { + list-style-type: none; + font-weight: normal; +} + +li.expanded_subject { + margin-left: .3em; +} + .twitter-tweet { border: 1px solid #ddd; padding: .5em .75em; diff --git a/zephyr/static/templates/sidebar_subject_list.handlebars b/zephyr/static/templates/sidebar_subject_list.handlebars new file mode 100644 index 0000000000..1bd1e55edd --- /dev/null +++ b/zephyr/static/templates/sidebar_subject_list.handlebars @@ -0,0 +1,7 @@ +