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',