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