From 7326971380b4351e5e954a33875a074661a1f5ba Mon Sep 17 00:00:00 2001 From: Steve Howell Date: Sun, 23 Apr 2017 19:11:25 -0700 Subject: [PATCH] Extract stream_edit.js. This code makes the right pane work in "Manage Streams" when you are editing a stream subscription. It handles basic functionality (submitting forms, etc.), live updates, and showing the pane as needed. Most of the code here was simply moved from subs.js, but some functions were pulled out of larger functions: live update: add_me_to_member_list update_stream_name update_stream_description collapse/show: collapse show_sub We also now export subs.show_subs_pane. We eventually want stream_edit not to call into subs.js, and this should be fairly easy--we just need to move some shared methods to a new module. --- .eslintrc.json | 1 + static/js/click_handlers.js | 4 +- static/js/compose.js | 2 +- static/js/settings_notifications.js | 4 +- static/js/stream_edit.js | 584 ++++++++++++++++++++++++++++ static/js/subs.js | 577 ++------------------------- tools/js-dep-visualizer.py | 5 + tools/lib/find_add_class.py | 1 + zproject/settings.py | 1 + 9 files changed, 620 insertions(+), 559 deletions(-) create mode 100644 static/js/stream_edit.js diff --git a/.eslintrc.json b/.eslintrc.json index 10739fa95d..293047dd7c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -58,6 +58,7 @@ "compose_fade": false, "modals": false, "stream_create": false, + "stream_edit": false, "subs": false, "stream_muting": false, "stream_events": false, diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js index 7d08689ff5..dae25754f2 100644 --- a/static/js/click_handlers.js +++ b/static/js/click_handlers.js @@ -466,8 +466,8 @@ $(function () { (function () { var map = { - ".stream-description-editable": subs.change_stream_description, - ".stream-name-editable": subs.change_stream_name, + ".stream-description-editable": stream_edit.change_stream_description, + ".stream-name-editable": stream_edit.change_stream_name, }; // http://stackoverflow.com/questions/4233265/contenteditable-set-caret-at-the-end-of-the-text-cross-browser diff --git a/static/js/compose.js b/static/js/compose.js index 582258e457..50fa587595 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -734,7 +734,7 @@ $(function () { return; } - subs.invite_user_to_stream(email, sub, success, failure); + stream_edit.invite_user_to_stream(email, sub, success, failure); }); $("#compose_invite_users").on('click', '.compose_invite_close', function (event) { diff --git a/static/js/settings_notifications.js b/static/js/settings_notifications.js index 47d4ec2b2c..f0f0ac91c9 100644 --- a/static/js/settings_notifications.js +++ b/static/js/settings_notifications.js @@ -103,12 +103,12 @@ exports.set_up = function () { function update_desktop_notification_setting(new_setting) { update_global_stream_setting("enable_stream_desktop_notifications", new_setting); - subs.set_all_stream_desktop_notifications_to(new_setting); + stream_edit.set_all_stream_desktop_notifications_to(new_setting); } function update_audible_notification_setting(new_setting) { update_global_stream_setting("enable_stream_sounds", new_setting); - subs.set_all_stream_audible_notifications_to(new_setting); + stream_edit.set_all_stream_audible_notifications_to(new_setting); } function maybe_bulk_update_stream_notification_setting(notification_checkbox, diff --git a/static/js/stream_edit.js b/static/js/stream_edit.js new file mode 100644 index 0000000000..4942e849c8 --- /dev/null +++ b/static/js/stream_edit.js @@ -0,0 +1,584 @@ +var stream_edit = (function () { + +var exports = {}; + +function setup_subscriptions_stream_hash(sub, stream_id) { + subs.change_state.prevent_once(); + window.location.hash = "#streams" + "/" + + stream_id + "/" + + hash_util.encodeHashComponent(sub.name); +} + +function settings_for_sub(sub) { + var id = parseInt(sub.stream_id, 10); + return $("#subscription_overlay .subscription_settings[data-stream-id='" + id + "']"); +} + +exports.collapse = function (sub) { + // I am not sure whether this code is really correct; it was extracted + // from subs.update_settings_for_unsubscribed() and possibly pre-dates + // our big streams re-design in late 2016. + var stream_settings = settings_for_sub(sub); + if (stream_settings.hasClass('in')) { + stream_settings.collapse('hide'); + } + + var sub_row = stream_settings.closest('.stream-row'); + sub_row.find(".regular_subscription_settings").removeClass('in'); +}; + +exports.show_sub = function (sub) { + var stream_settings = settings_for_sub(sub); + var sub_row = stream_settings.closest('.stream-row'); + sub_row.find(".regular_subscription_settings").addClass('in'); +}; + +exports.add_me_to_member_list = function (sub) { + // Add the user to the member list if they're currently + // viewing the members of this stream + var stream_settings = settings_for_sub(sub); + if (sub.render_subscribers && stream_settings.hasClass('in')) { + exports.prepend_subscriber( + stream_settings, + people.my_current_email()); + } +}; + +exports.show_stream_row = function (node, show_settings) { + $(".display-type #add_new_stream_title").hide(); + $(".display-type #stream_settings_title, .right .settings").show(); + $(".stream-row.active").removeClass("active"); + if (show_settings) { + subs.show_subs_pane.settings(); + + $(node).addClass("active"); + stream_edit.show_settings_for(node); + } else { + subs.show_subs_pane.nothing_selected(); + } +}; + +function format_member_list_elem(email) { + var person = people.get_by_email(email); + return templates.render('stream_member_list_entry', + {name: person.full_name, email: email, + displaying_for_admin: page_params.is_admin}); +} + +function get_subscriber_list(sub_row) { + var id = sub_row.data("stream-id"); + return $('.subscription_settings[data-stream-id="' + id + '"] .subscriber-list'); +} + +exports.update_stream_name = function (sub, new_name) { + var sub_settings = settings_for_sub(sub); + sub_settings.find(".email-address").text(sub.email_address); + sub_settings.find(".stream-name-editable").text(new_name); +}; + +exports.update_stream_description = function (sub) { + var stream_settings = settings_for_sub(sub); + stream_settings.find('input.description').val(sub.description); + stream_settings.find('.stream-description-editable').html(sub.rendered_description); +}; + +exports.prepend_subscriber = function (sub_row, email) { + var list = get_subscriber_list(sub_row); + list.prepend(format_member_list_elem(email)); +}; + +exports.invite_user_to_stream = function (user_email, sub, success, failure) { + // TODO: use stream_id when backend supports it + var stream_name = sub.name; + return channel.post({ + url: "/json/users/me/subscriptions", + data: {subscriptions: JSON.stringify([{name: stream_name}]), + principals: JSON.stringify([user_email])}, + success: success, + error: failure, + }); +}; + +exports.remove_user_from_stream = function (user_email, sub, success, failure) { + // TODO: use stream_id when backend supports it + var stream_name = sub.name; + return channel.del({ + url: "/json/users/me/subscriptions", + data: {subscriptions: JSON.stringify([stream_name]), + principals: JSON.stringify([user_email])}, + success: success, + error: failure, + }); +}; + +function get_stream_id(target) { + if (target.constructor !== jQuery) { + target = $(target); + } + return target.closest(".stream-row, .subscription_settings").attr("data-stream-id"); +} + +function get_sub_for_target(target) { + var stream_id = get_stream_id(target); + if (!stream_id) { + blueslip.error('Cannot find stream id for target'); + return; + } + + var sub = stream_data.get_sub_by_id(stream_id); + if (!sub) { + blueslip.error('get_sub_for_target() failed id lookup: ' + stream_id); + return; + } + return sub; +} + +function show_subscription_settings(sub_row) { + var stream_id = sub_row.data("stream-id"); + var sub = stream_data.get_sub_by_id(stream_id); + var sub_settings = settings_for_sub(sub); + var alerts = sub_settings + .find('.subscriber_list_container') + .find('.alert-warning, .alert-error'); + + var colorpicker = sub_settings.find('.colorpicker'); + var color = stream_data.get_color(sub.name); + stream_color.set_colorpicker_color(colorpicker, color); + + if (!sub.render_subscribers) { + return; + } + + // fetch subscriber list from memory. + var list = get_subscriber_list(sub_settings); + alerts.addClass("hide"); + list.empty(); + + var emails = []; + sub.subscribers.each(function (o, i) { + var email = people.get_person_from_user_id(i).email; + emails.push(format_member_list_elem(email)); + }); + + var list_html = emails.sort().reduce(function (accumulator, item) { + return accumulator + item; + }, ""); + + // wait for the next frame to append the list so other things can happen in + // the meanwhile. + window.requestAnimationFrame(function () { + list.append(list_html); + }); + + sub_settings.find('input[name="principal"]').typeahead({ + source: people.get_realm_persons, // This is a function. + items: 5, + highlighter: function (item) { + var item_formatted = typeahead_helper.render_person(item); + return typeahead_helper.highlight_with_escaping(this.query, item_formatted); + }, + matcher: function (item) { + var query = $.trim(this.query.toLowerCase()); + if (query === '' || query === item.email) { + return false; + } + // Case-insensitive. + return (item.email.toLowerCase().indexOf(query) !== -1) || + (item.full_name.toLowerCase().indexOf(query) !== -1); + }, + sorter: function (matches) { + var current_stream = compose_state.stream_name(); + return typeahead_helper.sort_recipientbox_typeahead( + this.query, matches, current_stream); + }, + updater: function (item) { + return item.email; + }, + }); +} + +exports.show_settings_for = function (node) { + var stream_id = get_stream_id(node); + var sub = stream_data.get_sub_by_id(stream_id); + var sub_settings = settings_for_sub(sub); + + var sub_row = $(".subscription_settings[data-stream-id='" + stream_id + "']"); + $(".subscription_settings[data-stream].show").removeClass("show"); + + $("#subscription_overlay .subscription_settings.show").removeClass("show"); + sub_settings.addClass("show"); + + show_subscription_settings(sub_row); +}; + +function stream_home_view_clicked(e) { + var sub = get_sub_for_target(e.target); + if (!sub) { + blueslip.error('stream_home_view_clicked() fails'); + return; + } + + var sub_settings = settings_for_sub(sub); + var notification_checkboxes = sub_settings.find(".sub_notification_setting"); + + subs.toggle_home(sub); + + if (sub.in_home_view) { + sub_settings.find(".mute-note").addClass("hide-mute-note"); + notification_checkboxes.removeClass("muted-sub"); + notification_checkboxes.find("input[type='checkbox']").removeAttr("disabled"); + } else { + sub_settings.find(".mute-note").removeClass("hide-mute-note"); + notification_checkboxes.addClass("muted-sub"); + notification_checkboxes.find("input[type='checkbox']").attr("disabled", true); + } +} + +exports.set_stream_property = function (sub, property, value) { + // TODO: Fix backend so it takes a stream id. + var stream_name = sub.name; + var sub_data = {stream: stream_name, property: property, value: value}; + return channel.post({ + url: '/json/subscriptions/property', + data: {subscription_data: JSON.stringify([sub_data])}, + timeout: 10*1000, + }); +}; + +function set_notification_setting_for_all_streams(notification_type, new_setting) { + _.each(stream_data.subscribed_subs(), function (sub) { + if (sub[notification_type] !== new_setting) { + exports.set_stream_property(sub, notification_type, new_setting); + } + }); +} + +exports.set_all_stream_desktop_notifications_to = function (new_setting) { + set_notification_setting_for_all_streams("desktop_notifications", new_setting); +}; + +exports.set_all_stream_audible_notifications_to = function (new_setting) { + set_notification_setting_for_all_streams("audible_notifications", new_setting); +}; + +function redraw_privacy_related_stuff(sub_row, sub) { + var stream_settings = settings_for_sub(sub); + var html; + + sub = stream_data.add_admin_options(sub); + + html = templates.render('subscription_setting_icon', sub); + sub_row.find('.icon').expectOne().replaceWith($(html)); + + html = templates.render('subscription_type', sub); + stream_settings.find('.subscription-type-text').expectOne().html(html); + + if (sub.invite_only) { + stream_settings.find(".large-icon") + .removeClass("hash").addClass("lock") + .html(""); + } else { + stream_settings.find(".large-icon") + .addClass("hash").removeClass("lock") + .html(""); + } + + stream_list.redraw_stream_privacy(sub.name); +} + +function change_stream_privacy(e) { + e.stopPropagation(); + + var stream_id = $(e.target).data("stream-id"); + var sub = stream_data.get_sub_by_id(stream_id); + + $("#subscriptions-status").hide(); + var data = { + stream_name: sub.name, + // toggle the privacy setting + is_private: !sub.invite_only, + }; + + channel.patch({ + url: "/json/streams/" + stream_id, + data: data, + success: function () { + sub = stream_data.get_sub_by_id(stream_id); + var sub_row = $(".stream-row[data-stream-id='" + stream_id + "']"); + + // save new privacy settings. + sub.invite_only = !sub.invite_only; + + redraw_privacy_related_stuff(sub_row, sub); + $("#stream_privacy_modal").remove(); + }, + error: function () { + $("#change-stream-privacy-button").text(i18n.t("Try again")); + }, + }); +} + +function stream_desktop_notifications_clicked(e) { + var sub = get_sub_for_target(e.target); + sub.desktop_notifications = ! sub.desktop_notifications; + exports.set_stream_property(sub, 'desktop_notifications', sub.desktop_notifications); +} + +function stream_audible_notifications_clicked(e) { + var sub = get_sub_for_target(e.target); + sub.audible_notifications = ! sub.audible_notifications; + exports.set_stream_property(sub, 'audible_notifications', sub.audible_notifications); +} + +function stream_pin_clicked(e) { + var sub = get_sub_for_target(e.target); + if (!sub) { + blueslip.error('stream_pin_clicked() fails'); + return; + } + subs.toggle_pin_to_top_stream(sub); +} + +exports.change_stream_name = function (e) { + e.preventDefault(); + var sub_settings = $(e.target).closest('.subscription_settings'); + var stream_id = $(e.target).closest(".subscription_settings").attr("data-stream-id"); + var new_name_box = sub_settings.find('.stream-name-editable'); + var new_name = $.trim(new_name_box.text()); + + $("#subscriptions-status").hide(); + + channel.patch({ + // Stream names might contain unsafe characters so we must encode it first. + url: "/json/streams/" + stream_id, + data: {new_name: JSON.stringify(new_name)}, + success: function () { + new_name_box.val(''); + ui_report.success(i18n.t("The stream has been renamed!"), $("#subscriptions-status "), + 'subscriptions-status'); + }, + error: function (xhr) { + ui_report.error(i18n.t("Error renaming stream"), xhr, + $("#subscriptions-status"), 'subscriptions-status'); + }, + }); +}; + +exports.change_stream_description = function (e) { + e.preventDefault(); + + var sub_settings = $(e.target).closest('.subscription_settings'); + var sub = get_sub_for_target(sub_settings); + if (!sub) { + blueslip.error('change_stream_description() fails'); + return; + } + + var stream_id = sub.stream_id; + var description = sub_settings.find('.stream-description-editable').text().trim(); + + $('#subscriptions-status').hide(); + + channel.patch({ + // Stream names might contain unsafe characters so we must encode it first. + url: '/json/streams/' + stream_id, + data: { + description: JSON.stringify(description), + }, + success: function () { + // The event from the server will update the rest of the UI + ui_report.success(i18n.t("The stream description has been updated!"), + $("#subscriptions-status"), 'subscriptions-status'); + }, + error: function (xhr) { + ui_report.error(i18n.t("Error updating the stream description"), xhr, + $("#subscriptions-status"), 'subscriptions-status'); + }, + }); +}; + +$(function () { + $("#zfilt").on("click", ".stream_sub_unsub_button", function (e) { + e.preventDefault(); + e.stopPropagation(); + + var stream_name = narrow.stream(); + if (stream_name === undefined) { + return; + } + var sub = stream_data.get_sub(stream_name); + subs.sub_or_unsub(sub); + }); + + $("#subscriptions_table").on("click", ".change-stream-privacy", function (e) { + var stream_id = get_stream_id(e.target); + var stream = stream_data.get_sub_by_id(stream_id); + var template_data = { + is_private: stream.can_make_public, + stream_id: stream_id, + }; + var change_privacy_modal = templates.render("subscription_stream_privacy_modal", template_data); + + $("#subscriptions_table").append(change_privacy_modal); + + $("#change-stream-privacy-button").click(function (e) { + change_stream_privacy(e); + }); + }); + + $("#subscriptions_table").on("click", ".close-privacy-modal", function () { + $("#stream_privacy_modal").remove(); + }); + + $("#subscriptions_table").on("click", "#sub_setting_not_in_home_view", + stream_home_view_clicked); + $("#subscriptions_table").on("click", "#sub_desktop_notifications_setting", + 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(); + var settings_row = $(e.target).closest('.subscription_settings'); + var sub = get_sub_for_target(settings_row); + if (!sub) { + blueslip.error('.subscriber_list_add form submit fails'); + return; + } + + var text_box = settings_row.find('input[name="principal"]'); + var principal = $.trim(text_box.val()); + // TODO: clean up this error handling + var error_elem = settings_row.find('.subscriber_list_container .alert-error'); + var warning_elem = settings_row.find('.subscriber_list_container .alert-warning'); + + function invite_success(data) { + text_box.val(''); + + if (data.subscribed.hasOwnProperty(principal)) { + error_elem.addClass("hide"); + warning_elem.addClass("hide"); + if (people.is_current_user(principal)) { + // mark_subscribed adds the user to the member list + // TODO: We should really let the event system + // handle this, as mark_subscribed has + // lots of side effects. + stream_events.mark_subscribed(sub); + } + } else { + error_elem.addClass("hide"); + warning_elem.removeClass("hide").text(i18n.t("User already subscribed")); + } + } + + function invite_failure() { + warning_elem.addClass("hide"); + error_elem.removeClass("hide").text(i18n.t("Could not add user to this stream")); + } + + exports.invite_user_to_stream(principal, sub, invite_success, invite_failure); + }); + + $("#subscriptions_table").on("submit", ".subscriber_list_remove form", function (e) { + e.preventDefault(); + + var list_entry = $(e.target).closest("tr"); + var principal = list_entry.children(".subscriber-email").text(); + var settings_row = $(e.target).closest('.subscription_settings'); + + var sub = get_sub_for_target(settings_row); + if (!sub) { + blueslip.error('.subscriber_list_remove form submit fails'); + return; + } + + var error_elem = settings_row.find('.subscriber_list_container .alert-error'); + var warning_elem = settings_row.find('.subscriber_list_container .alert-warning'); + + function removal_success(data) { + if (data.removed.length > 0) { + error_elem.addClass("hide"); + warning_elem.addClass("hide"); + + // Remove the user from the subscriber list. + list_entry.remove(); + + if (people.is_current_user(principal)) { + // If you're unsubscribing yourself, mark whole + // stream entry as you being unsubscribed. + // TODO: We should really let the event system + // handle this, as mark_unsubscribed has + // lots of side effects. + stream_events.mark_unsubscribed(sub); + } + } else { + error_elem.addClass("hide"); + warning_elem.removeClass("hide").text(i18n.t("User is already not subscribed")); + } + } + + function removal_failure() { + warning_elem.addClass("hide"); + error_elem.removeClass("hide").text(i18n.t("Error removing user from this stream")); + } + + exports.remove_user_from_stream(principal, sub, removal_success, + removal_failure); + }); + + // This handler isn't part of the normal edit interface; it's the convenient + // checkmark in the subscriber list. + $("#subscriptions_table").on("click", ".sub_unsub_button", function (e) { + var sub = get_sub_for_target(e.target); + var stream_row = $(this).parent(); + var stream_id = stream_row.attr("data-stream-id"); + subs.sub_or_unsub(sub); + var sub_settings = settings_for_sub(sub); + var regular_sub_settings = sub_settings.find(".regular_subscription_settings"); + if (!sub.subscribed) { + regular_sub_settings.addClass("in"); + exports.show_stream_row(stream_row, true); + } else { + regular_sub_settings.removeClass("in"); + } + + setup_subscriptions_stream_hash(sub, stream_id); + e.preventDefault(); + e.stopPropagation(); + }); + + $("#subscriptions_table").on("click", ".stream-row", function (e) { + if ($(e.target).closest(".check, .subscription_settings").length === 0) { + exports.show_stream_row(this, true); + var stream_id = $(this).attr("data-stream-id"); + var sub = stream_data.get_sub_by_id(stream_id); + setup_subscriptions_stream_hash(sub, stream_id); + } + }); + + $(document).on('peer_subscribe.zulip', function (e, data) { + var sub = stream_data.get_sub(data.stream_name); + var sub_row = settings_for_sub(sub); + exports.prepend_subscriber(sub_row, data.user_email); + }); + + $(document).on('peer_unsubscribe.zulip', function (e, data) { + var sub = stream_data.get_sub(data.stream_name); + + var sub_row = settings_for_sub(sub); + var tr = sub_row.find("tr[data-subscriber-email='" + + data.user_email + + "']"); + tr.remove(); + }); + +}); + +return exports; + +}()); +if (typeof module !== 'undefined') { + module.exports = stream_edit; +} diff --git a/static/js/subs.js b/static/js/subs.js index 7c81edab2c..98a5e0f29f 100644 --- a/static/js/subs.js +++ b/static/js/subs.js @@ -6,10 +6,16 @@ var meta = { }; var exports = {}; -function settings_for_sub(sub) { - var id = parseInt(sub.stream_id, 10); - return $("#subscription_overlay .subscription_settings[data-stream-id='" + id + "']"); -} +exports.show_subs_pane = { + nothing_selected: function () { + $(".nothing-selected, #stream_settings_title").show(); + $("#add_new_stream_title, .settings, #stream-creation").hide(); + }, + settings: function () { + $(".settings, #stream_settings_title").show(); + $("#add_new_stream_title, #stream-creation, .nothing-selected").hide(); + }, +}; function button_for_sub(sub) { var id = parseInt(sub.stream_id, 10); @@ -76,85 +82,13 @@ function should_list_all_streams() { return !page_params.is_zephyr_mirror_realm; } -function set_stream_property(sub, property, value) { - // TODO: Fix backend so it takes a stream id. - var stream_name = sub.name; - var sub_data = {stream: stream_name, property: property, value: value}; - return channel.post({ - url: '/json/subscriptions/property', - data: {subscription_data: JSON.stringify([sub_data])}, - timeout: 10*1000, - }); -} - -function set_notification_setting_for_all_streams(notification_type, new_setting) { - _.each(stream_data.subscribed_subs(), function (sub) { - if (sub[notification_type] !== new_setting) { - set_stream_property(sub, notification_type, new_setting); - } - }); -} - -exports.set_all_stream_desktop_notifications_to = function (new_setting) { - set_notification_setting_for_all_streams("desktop_notifications", new_setting); -}; - -exports.set_all_stream_audible_notifications_to = function (new_setting) { - set_notification_setting_for_all_streams("audible_notifications", new_setting); -}; - -function get_stream_id(target) { - if (target.constructor !== jQuery) { - target = $(target); - } - return target.closest(".stream-row, .subscription_settings").attr("data-stream-id"); -} - -function get_sub_for_target(target) { - var stream_id = get_stream_id(target); - if (!stream_id) { - blueslip.error('Cannot find stream id for target'); - return; - } - - var sub = stream_data.get_sub_by_id(stream_id); - if (!sub) { - blueslip.error('get_sub_for_target() failed id lookup: ' + stream_id); - return; - } - return sub; -} - -function stream_home_view_clicked(e) { - var sub = get_sub_for_target(e.target); - if (!sub) { - blueslip.error('stream_home_view_clicked() fails'); - return; - } - - var sub_settings = settings_for_sub(sub); - var notification_checkboxes = sub_settings.find(".sub_notification_setting"); - - subs.toggle_home(sub); - - if (sub.in_home_view) { - sub_settings.find(".mute-note").addClass("hide-mute-note"); - notification_checkboxes.removeClass("muted-sub"); - notification_checkboxes.find("input[type='checkbox']").removeAttr("disabled"); - } else { - sub_settings.find(".mute-note").removeClass("hide-mute-note"); - notification_checkboxes.addClass("muted-sub"); - notification_checkboxes.find("input[type='checkbox']").attr("disabled", true); - } -} - exports.toggle_home = function (sub) { stream_muting.update_in_home_view(sub, ! sub.in_home_view); - set_stream_property(sub, 'in_home_view', sub.in_home_view); + stream_edit.set_stream_property(sub, 'in_home_view', sub.in_home_view); }; exports.toggle_pin_to_top_stream = function (sub) { - set_stream_property(sub, 'pin_to_top', !sub.pin_to_top); + stream_edit.set_stream_property(sub, 'pin_to_top', !sub.pin_to_top); }; exports.update_stream_name = function (sub, new_name) { @@ -166,9 +100,7 @@ exports.update_stream_name = function (sub, new_name) { stream_list.rename_stream(sub, new_name); // Update the stream settings - var sub_settings = settings_for_sub(stream_data.get_sub_by_id(stream_id)); - sub_settings.find(".email-address").text(sub.email_address); - sub_settings.find(".stream-name-editable").text(new_name); + stream_edit.update_stream_name(sub, new_name); // Update the subscriptions page var sub_row = $(".stream-row[data-stream-id='" + stream_id + "']"); @@ -187,35 +119,12 @@ exports.update_stream_description = function (sub, description) { sub_row.find(".description").html(sub.rendered_description); // Update stream settings - var stream_settings = settings_for_sub(sub); - stream_settings.find('input.description').val(sub.description); - stream_settings.find('.stream-description-editable').html(sub.rendered_description); + stream_edit.update_stream_description(sub); }; -function stream_desktop_notifications_clicked(e) { - var sub = get_sub_for_target(e.target); - sub.desktop_notifications = ! sub.desktop_notifications; - set_stream_property(sub, 'desktop_notifications', sub.desktop_notifications); -} - -function stream_audible_notifications_clicked(e) { - var sub = get_sub_for_target(e.target); - sub.audible_notifications = ! sub.audible_notifications; - set_stream_property(sub, 'audible_notifications', sub.audible_notifications); -} - -function stream_pin_clicked(e) { - var sub = get_sub_for_target(e.target); - if (!sub) { - blueslip.error('stream_pin_clicked() fails'); - return; - } - exports.toggle_pin_to_top_stream(sub); -} - exports.set_color = function (stream_id, color) { var sub = stream_data.get_sub_by_id(stream_id); - set_stream_property(sub, 'color', color); + stream_edit.set_stream_property(sub, 'color', color); }; exports.rerender_subscribers_count = function (sub) { @@ -271,23 +180,6 @@ function add_sub_to_table(sub) { } } -function format_member_list_elem(email) { - var person = people.get_by_email(email); - return templates.render('stream_member_list_entry', - {name: person.full_name, email: email, - displaying_for_admin: page_params.is_admin}); -} - -function get_subscriber_list(sub_row) { - var id = sub_row.data("stream-id"); - return $('.subscription_settings[data-stream-id="' + id + '"] .subscriber-list'); -} - -function prepend_subscriber(sub_row, email) { - var list = get_subscriber_list(sub_row); - list.prepend(format_member_list_elem(email)); -} - exports.remove_stream = function (stream_id) { // It is possible that row is empty when we deactivate a // stream, but we let jQuery silently handle that. @@ -295,85 +187,7 @@ exports.remove_stream = function (stream_id) { row.remove(); }; -function show_subscription_settings(sub_row) { - var stream_id = sub_row.data("stream-id"); - var sub = stream_data.get_sub_by_id(stream_id); - var sub_settings = settings_for_sub(sub); - var alerts = sub_settings - .find('.subscriber_list_container') - .find('.alert-warning, .alert-error'); - - var colorpicker = sub_settings.find('.colorpicker'); - var color = stream_data.get_color(sub.name); - stream_color.set_colorpicker_color(colorpicker, color); - - if (!sub.render_subscribers) { - return; - } - - // fetch subscriber list from memory. - var list = get_subscriber_list(sub_settings); - alerts.addClass("hide"); - list.empty(); - - var emails = []; - sub.subscribers.each(function (o, i) { - var email = people.get_person_from_user_id(i).email; - emails.push(format_member_list_elem(email)); - }); - - var list_html = emails.sort().reduce(function (accumulator, item) { - return accumulator + item; - }, ""); - - // wait for the next frame to append the list so other things can happen in - // the meanwhile. - window.requestAnimationFrame(function () { - list.append(list_html); - }); - - sub_settings.find('input[name="principal"]').typeahead({ - source: people.get_realm_persons, // This is a function. - items: 5, - highlighter: function (item) { - var item_formatted = typeahead_helper.render_person(item); - return typeahead_helper.highlight_with_escaping(this.query, item_formatted); - }, - matcher: function (item) { - var query = $.trim(this.query.toLowerCase()); - if (query === '' || query === item.email) { - return false; - } - // Case-insensitive. - return (item.email.toLowerCase().indexOf(query) !== -1) || - (item.full_name.toLowerCase().indexOf(query) !== -1); - }, - sorter: function (matches) { - var current_stream = compose_state.stream_name(); - return typeahead_helper.sort_recipientbox_typeahead( - this.query, matches, current_stream); - }, - updater: function (item) { - return item.email; - }, - }); -} - -exports.show_settings_for = function (stream_id) { - var sub = stream_data.get_sub_by_id(stream_id); - var sub_settings = settings_for_sub(sub); - - var sub_row = $(".subscription_settings[data-stream-id='" + stream_id + "']"); - $(".subscription_settings[data-stream].show").removeClass("show"); - - $("#subscription_overlay .subscription_settings.show").removeClass("show"); - sub_settings.addClass("show"); - - show_subscription_settings(sub_row); -}; - exports.update_settings_for_subscribed = function (sub) { - var stream_settings = settings_for_sub(sub); var button = button_for_sub(sub); var settings_button = settings_button_for_sub(sub).removeClass("unsubscribed"); @@ -382,19 +196,14 @@ exports.update_settings_for_subscribed = function (sub) { button.toggleClass("checked"); settings_button.text(i18n.t("Unsubscribe")); - // Add the user to the member list if they're currently - // viewing the members of this stream - if (sub.render_subscribers && stream_settings.hasClass('in')) { - prepend_subscriber(stream_settings, - people.my_current_email()); - } + + stream_edit.add_me_to_member_list(sub); } else { add_sub_to_table(sub); } // Display the swatch and subscription stream_settings - var sub_row = stream_settings.closest('.stream-row'); - sub_row.find(".regular_subscription_settings").addClass('in'); + stream_edit.show_sub(sub); }; exports.update_settings_for_unsubscribed = function (sub) { @@ -404,16 +213,10 @@ exports.update_settings_for_unsubscribed = function (sub) { button.toggleClass("checked"); settings_button.text(i18n.t("Subscribe")); - var stream_settings = settings_for_sub(sub); - if (stream_settings.hasClass('in')) { - stream_settings.collapse('hide'); - } - exports.rerender_subscribers_count(sub); - // Hide the swatch and subscription settings - var sub_row = stream_settings.closest('.stream-row'); - sub_row.find(".regular_subscription_settings").removeClass('in'); + stream_edit.collapse(sub); + row_for_stream_id(subs.stream_id).attr("data-temp-view", true); }; @@ -518,63 +321,6 @@ function actually_filter_streams() { exports.filter_table({ input: query, subscribed_only: subscribed_only }); } -function redraw_privacy_related_stuff(sub_row, sub) { - var stream_settings = settings_for_sub(sub); - var html; - - sub = stream_data.add_admin_options(sub); - - html = templates.render('subscription_setting_icon', sub); - sub_row.find('.icon').expectOne().replaceWith($(html)); - - html = templates.render('subscription_type', sub); - stream_settings.find('.subscription-type-text').expectOne().html(html); - - if (sub.invite_only) { - stream_settings.find(".large-icon") - .removeClass("hash").addClass("lock") - .html(""); - } else { - stream_settings.find(".large-icon") - .addClass("hash").removeClass("lock") - .html(""); - } - - stream_list.redraw_stream_privacy(sub.name); -} - -function change_stream_privacy(e) { - e.stopPropagation(); - - var stream_id = $(e.target).data("stream-id"); - var sub = stream_data.get_sub_by_id(stream_id); - - $("#subscriptions-status").hide(); - var data = { - stream_name: sub.name, - // toggle the privacy setting - is_private: !sub.invite_only, - }; - - channel.patch({ - url: "/json/streams/" + stream_id, - data: data, - success: function () { - sub = stream_data.get_sub_by_id(stream_id); - var sub_row = $(".stream-row[data-stream-id='" + stream_id + "']"); - - // save new privacy settings. - sub.invite_only = !sub.invite_only; - - redraw_privacy_related_stuff(sub_row, sub); - $("#stream_privacy_modal").remove(); - }, - error: function () { - $("#change-stream-privacy-button").text(i18n.t("Try again")); - }, - }); -} - var filter_streams = _.throttle(actually_filter_streams, 50); exports.setup_page = function (callback) { @@ -847,88 +593,6 @@ exports.new_stream_clicked = function () { stream_create.new_stream_clicked(stream); }; -exports.invite_user_to_stream = function (user_email, sub, success, failure) { - // TODO: use stream_id when backend supports it - var stream_name = sub.name; - return channel.post({ - url: "/json/users/me/subscriptions", - data: {subscriptions: JSON.stringify([{name: stream_name}]), - principals: JSON.stringify([user_email])}, - success: success, - error: failure, - }); -}; - -exports.remove_user_from_stream = function (user_email, sub, success, failure) { - // TODO: use stream_id when backend supports it - var stream_name = sub.name; - return channel.del({ - url: "/json/users/me/subscriptions", - data: {subscriptions: JSON.stringify([stream_name]), - principals: JSON.stringify([user_email])}, - success: success, - error: failure, - }); -}; - -exports.change_stream_description = function (e) { - e.preventDefault(); - - var sub_settings = $(e.target).closest('.subscription_settings'); - var sub = get_sub_for_target(sub_settings); - if (!sub) { - blueslip.error('change_stream_description() fails'); - return; - } - - var stream_id = sub.stream_id; - var description = sub_settings.find('.stream-description-editable').text().trim(); - - $('#subscriptions-status').hide(); - - channel.patch({ - // Stream names might contain unsafe characters so we must encode it first. - url: '/json/streams/' + stream_id, - data: { - description: JSON.stringify(description), - }, - success: function () { - // The event from the server will update the rest of the UI - ui_report.success(i18n.t("The stream description has been updated!"), - $("#subscriptions-status"), 'subscriptions-status'); - }, - error: function (xhr) { - ui_report.error(i18n.t("Error updating the stream description"), xhr, - $("#subscriptions-status"), 'subscriptions-status'); - }, - }); -}; - -exports.change_stream_name = function (e) { - e.preventDefault(); - var sub_settings = $(e.target).closest('.subscription_settings'); - var stream_id = $(e.target).closest(".subscription_settings").attr("data-stream-id"); - var new_name_box = sub_settings.find('.stream-name-editable'); - var new_name = $.trim(new_name_box.text()); - - $("#subscriptions-status").hide(); - - channel.patch({ - // Stream names might contain unsafe characters so we must encode it first. - url: "/json/streams/" + stream_id, - data: {new_name: JSON.stringify(new_name)}, - success: function () { - new_name_box.val(''); - ui_report.success(i18n.t("The stream has been renamed!"), $("#subscriptions-status "), - 'subscriptions-status'); - }, - error: function (xhr) { - ui_report.error(i18n.t("Error renaming stream"), xhr, - $("#subscriptions-status"), 'subscriptions-status'); - }, - }); -}; - exports.sub_or_unsub = function (sub) { if (sub.subscribed) { ajaxUnsubscribe(sub); @@ -947,17 +611,6 @@ $(function () { // when new messages come in, but it's fairly quick. stream_list.build_stream_list(); - var show_subs_pane = { - nothing_selected: function () { - $(".nothing-selected, #stream_settings_title").show(); - $("#add_new_stream_title, .settings, #stream-creation").hide(); - }, - settings: function () { - $(".settings, #stream_settings_title").show(); - $("#add_new_stream_title, #stream-creation, .nothing-selected").hide(); - }, - }; - $("#subscriptions_table").on("click", "#create_stream_button", function (e) { e.preventDefault(); exports.new_stream_clicked(); @@ -969,7 +622,7 @@ $(function () { // click; this fixes an issue where hitting "enter" would // trigger this code path due to bootstrap magic. if (e.clientY !== 0) { - show_subs_pane.nothing_selected(); + exports.show_subs_pane.nothing_selected(); } }); @@ -987,58 +640,6 @@ $(function () { selectText(this); }); - function setup_subscriptions_stream_hash(sub, stream_id) { - exports.change_state.prevent_once(); - window.location.hash = "#streams" + "/" + - stream_id + "/" + - hash_util.encodeHashComponent(sub.name); - } - - function show_stream_row(node, show_settings) { - $(".display-type #add_new_stream_title").hide(); - $(".display-type #stream_settings_title, .right .settings").show(); - $(".stream-row.active").removeClass("active"); - if (show_settings) { - show_subs_pane.settings(); - - $(node).addClass("active"); - exports.show_settings_for(get_stream_id(node)); - } else { - show_subs_pane.nothing_selected(); - } - } - - $("#subscriptions_table").on("click", ".sub_unsub_button", function (e) { - var sub = get_sub_for_target(e.target); - var stream_row = $(this).parent(); - var stream_id = stream_row.attr("data-stream-id"); - exports.sub_or_unsub(sub); - var sub_settings = settings_for_sub(sub); - var regular_sub_settings = sub_settings.find(".regular_subscription_settings"); - if (!sub.subscribed) { - regular_sub_settings.addClass("in"); - show_stream_row(stream_row, true); - } else { - regular_sub_settings.removeClass("in"); - } - - setup_subscriptions_stream_hash(sub, stream_id); - e.preventDefault(); - e.stopPropagation(); - }); - - $("#zfilt").on("click", ".stream_sub_unsub_button", function (e) { - e.preventDefault(); - e.stopPropagation(); - - var stream_name = narrow.stream(); - if (stream_name === undefined) { - return; - } - var sub = stream_data.get_sub(stream_name); - exports.sub_or_unsub(sub); - }); - $('.empty_feed_sub_unsub').click(function (e) { e.preventDefault(); @@ -1072,142 +673,17 @@ $(function () { control.prop("checked", ! control.prop("checked")); } }); - $("#subscriptions_table").on("click", "#sub_setting_not_in_home_view", stream_home_view_clicked); - $("#subscriptions_table").on("click", "#sub_desktop_notifications_setting", - 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(); - var settings_row = $(e.target).closest('.subscription_settings'); - var sub = get_sub_for_target(settings_row); - if (!sub) { - blueslip.error('.subscriber_list_add form submit fails'); - return; - } - - var text_box = settings_row.find('input[name="principal"]'); - var principal = $.trim(text_box.val()); - // TODO: clean up this error handling - var error_elem = settings_row.find('.subscriber_list_container .alert-error'); - var warning_elem = settings_row.find('.subscriber_list_container .alert-warning'); - - function invite_success(data) { - text_box.val(''); - - if (data.subscribed.hasOwnProperty(principal)) { - error_elem.addClass("hide"); - warning_elem.addClass("hide"); - if (people.is_current_user(principal)) { - // mark_subscribed adds the user to the member list - // TODO: We should really let the event system - // handle this, as mark_subscribed has - // lots of side effects. - stream_events.mark_subscribed(sub); - } - } else { - error_elem.addClass("hide"); - warning_elem.removeClass("hide").text(i18n.t("User already subscribed")); - } - } - - function invite_failure() { - warning_elem.addClass("hide"); - error_elem.removeClass("hide").text(i18n.t("Could not add user to this stream")); - } - - exports.invite_user_to_stream(principal, sub, invite_success, invite_failure); - }); - - $("#subscriptions_table").on("click", ".stream-row", function (e) { - if ($(e.target).closest(".check, .subscription_settings").length === 0) { - show_stream_row(this, true); - var stream_id = $(this).attr("data-stream-id"); - var sub = stream_data.get_sub_by_id(stream_id); - setup_subscriptions_stream_hash(sub, stream_id); - } - }); (function defocus_sub_settings() { var sel = ".search-container, .streams-list, .subscriptions-header"; $("#subscriptions_table").on("click", sel, function (e) { if ($(e.target).is(sel)) { - show_stream_row(this, false); + stream_edit.show_stream_row(this, false); } }); }()); - $("#subscriptions_table").on("submit", ".subscriber_list_remove form", function (e) { - e.preventDefault(); - - var list_entry = $(e.target).closest("tr"); - var principal = list_entry.children(".subscriber-email").text(); - var settings_row = $(e.target).closest('.subscription_settings'); - - var sub = get_sub_for_target(settings_row); - if (!sub) { - blueslip.error('.subscriber_list_remove form submit fails'); - return; - } - - var error_elem = settings_row.find('.subscriber_list_container .alert-error'); - var warning_elem = settings_row.find('.subscriber_list_container .alert-warning'); - - function removal_success(data) { - if (data.removed.length > 0) { - error_elem.addClass("hide"); - warning_elem.addClass("hide"); - - // Remove the user from the subscriber list. - list_entry.remove(); - - if (people.is_current_user(principal)) { - // If you're unsubscribing yourself, mark whole - // stream entry as you being unsubscribed. - // TODO: We should really let the event system - // handle this, as mark_unsubscribed has - // lots of side effects. - stream_events.mark_unsubscribed(sub); - } - } else { - error_elem.addClass("hide"); - warning_elem.removeClass("hide").text(i18n.t("User is already not subscribed")); - } - } - - function removal_failure() { - warning_elem.addClass("hide"); - error_elem.removeClass("hide").text(i18n.t("Error removing user from this stream")); - } - - exports.remove_user_from_stream(principal, sub, removal_success, - removal_failure); - }); - - $("#subscriptions_table").on("click", ".change-stream-privacy", function (e) { - var stream_id = get_stream_id(e.target); - var stream = stream_data.get_sub_by_id(stream_id); - var template_data = { - is_private: stream.can_make_public, - stream_id: stream_id, - }; - var change_privacy_modal = templates.render("subscription_stream_privacy_modal", template_data); - - $("#subscriptions_table").append(change_privacy_modal); - - $("#change-stream-privacy-button").click(function (e) { - change_stream_privacy(e); - }); - }); - - $("#subscriptions_table").on("click", ".close-privacy-modal", function () { - $("#stream_privacy_modal").remove(); - }); - $("#subscriptions_table").on("hide", ".subscription_settings", function (e) { var sub_arrow = $(e.target).closest('.stream-row').find('.sub_arrow i'); sub_arrow.removeClass('icon-vector-chevron-up'); @@ -1217,18 +693,11 @@ $(function () { $(document).on('peer_subscribe.zulip', function (e, data) { var sub = stream_data.get_sub(data.stream_name); exports.rerender_subscribers_count(sub); - var sub_row = settings_for_sub(sub); - prepend_subscriber(sub_row, data.user_email); }); + $(document).on('peer_unsubscribe.zulip', function (e, data) { var sub = stream_data.get_sub(data.stream_name); exports.rerender_subscribers_count(sub); - - var sub_row = settings_for_sub(sub); - var tr = sub_row.find("tr[data-subscriber-email='" + - data.user_email + - "']"); - tr.remove(); }); function subscriptions_close_modal() { diff --git a/tools/js-dep-visualizer.py b/tools/js-dep-visualizer.py index bfdf0eaa71..c08763da69 100644 --- a/tools/js-dep-visualizer.py +++ b/tools/js-dep-visualizer.py @@ -137,6 +137,9 @@ def find_edges_to_remove(graph, methods): ('compose_actions', 'resize'), ('settings_streams', 'stream_data'), ('drafts', 'hashchange'), + ('settings_notifications', 'stream_edit'), + ('compose', 'stream_edit'), + ('subs', 'stream_edit'), ] # type: List[Edge] def is_exempt(edge): @@ -152,6 +155,7 @@ def find_edges_to_remove(graph, methods): return edge in EXEMPT_EDGES APPROVED_CUTS = [ + ('stream_edit', 'stream_events'), ('unread_ui', 'pointer'), ('typing_events', 'narrow'), ('echo', 'message_events'), @@ -199,6 +203,7 @@ def find_edges_to_remove(graph, methods): ('muting_ui', 'stream_popover'), ('popovers', 'stream_popover'), ('topic_list', 'stream_popover'), + ('stream_edit', 'subs'), ('topic_list', 'narrow'), ('stream_list', 'narrow'), ('stream_list', 'pm_list'), diff --git a/tools/lib/find_add_class.py b/tools/lib/find_add_class.py index da7a6433dd..eee2d75916 100644 --- a/tools/lib/find_add_class.py +++ b/tools/lib/find_add_class.py @@ -15,6 +15,7 @@ GENERIC_KEYWORDS = [ 'error', 'expanded', 'hide', + 'in', 'show', 'notdisplayed', 'popover', diff --git a/zproject/settings.py b/zproject/settings.py index 23277f5032..d569066446 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -868,6 +868,7 @@ JS_SPECS = { 'js/stream_muting.js', 'js/stream_events.js', 'js/stream_create.js', + 'js/stream_edit.js', 'js/subs.js', 'js/message_edit.js', 'js/condense.js',