zulip/static/js/stream_create.js

464 lines
17 KiB
JavaScript
Raw Normal View History

const render_announce_stream_docs = require('../templates/announce_stream_docs.hbs');
const render_new_stream_users = require('../templates/new_stream_users.hbs');
const render_subscription_invites_warning_modal = require('../templates/subscription_invites_warning_modal.hbs');
let created_stream;
exports.reset_created_stream = function () {
created_stream = undefined;
};
exports.set_name = function (stream) {
created_stream = stream;
};
exports.get_name = function () {
return created_stream;
};
const stream_subscription_error = (function () {
const self = {};
self.report_no_subs_to_stream = function () {
$("#stream_subscription_error").text(i18n.t("You cannot create a stream with no subscribers!"));
$("#stream_subscription_error").show();
};
self.cant_create_stream_without_susbscribing = function () {
$("#stream_subscription_error").text(i18n.t("You must be an organization administrator to create a stream without subscribing."));
$("#stream_subscription_error").show();
};
self.clear_errors = function () {
$("#stream_subscription_error").hide();
};
return self;
}());
const stream_name_error = (function () {
const self = {};
self.report_already_exists = function () {
$("#stream_name_error").text(i18n.t("A stream with this name already exists"));
$("#stream_name_error").show();
};
self.clear_errors = function () {
$("#stream_name_error").hide();
};
self.report_empty_stream = function () {
$("#stream_name_error").text(i18n.t("A stream needs to have a name"));
$("#stream_name_error").show();
};
self.select = function () {
$("#create_stream_name").focus().select();
};
self.pre_validate = function (stream_name) {
// Don't worry about empty strings...we just want to call this
// to warn users early before they start doing too much work
// after they make the effort to type in a stream name. (The
// use case here is that I go to create a stream, only to find
// out it already exists, and I was just too lazy to look at
// the public streams that I'm not subscribed to yet. Once I
// realize the stream already exists, I may want to cancel.)
if (stream_name && stream_data.get_sub(stream_name)) {
self.report_already_exists();
return;
}
self.clear_errors();
};
self.validate_for_submit = function (stream_name) {
if (!stream_name) {
self.report_empty_stream();
self.select();
return false;
}
if (stream_data.get_sub(stream_name)) {
self.report_already_exists();
self.select();
return false;
}
// If we got this far, then we think we have a new unique stream
// name, so we'll submit to the server. (It's still plausible,
// however, that there's some invite-only stream that we don't
// know about locally that will cause a name collision.)
return true;
};
return self;
}());
// Within the new stream modal...
function update_announce_stream_state() {
// If there is no notifications_stream, we simply hide the widget.
if (!page_params.notifications_stream) {
$('#announce-new-stream').hide();
return;
}
// If the stream is invite only, disable the "Announce stream" option.
// Otherwise enable it.
const announce_stream_checkbox = $('#announce-new-stream input');
const announce_stream_label = $('#announce-new-stream');
let disable_it = false;
const privacy_type = $('input:radio[name=privacy]:checked').val();
const is_invite_only = privacy_type === "invite-only" || privacy_type === "invite-only-public-history";
announce_stream_label.removeClass("control-label-disabled");
if (is_invite_only) {
disable_it = true;
announce_stream_checkbox.prop('checked', false);
announce_stream_label.addClass("control-label-disabled");
}
announce_stream_checkbox.prop('disabled', disable_it);
$('#announce-new-stream').show();
}
2017-07-08 04:04:55 +02:00
function get_principals() {
return Array.from($("#stream_creation_form input:checkbox[name=user]:checked"), elem => {
const label = $(elem).closest(".add-user-label");
return parseInt(label.attr("data-user-id"), 10);
});
2017-07-08 04:04:55 +02:00
}
2017-07-08 04:07:36 +02:00
function create_stream() {
const data = {};
const stream_name = $.trim($("#create_stream_name").val());
const description = $.trim($("#create_stream_description").val());
created_stream = stream_name;
// Even though we already check to make sure that while typing the user cannot enter
// newline characters (by pressing the enter key) it would still be possible to copy
// and paste over a description with newline characters in it. Prevent that.
if (description.includes('\n')) {
ui_report.message(i18n.t("The stream description cannot contain newline characters."),
$(".stream_create_info"), 'alert-error');
return;
}
data.subscriptions = JSON.stringify([{name: stream_name, description: description}]);
2017-07-08 04:07:36 +02:00
let invite_only;
let history_public_to_subscribers;
const privacy_setting = $('#stream_creation_form input[name=privacy]:checked').val();
if (privacy_setting === 'invite-only') {
invite_only = true;
history_public_to_subscribers = false;
} else if (privacy_setting === 'invite-only-public-history') {
invite_only = true;
history_public_to_subscribers = true;
} else {
invite_only = false;
history_public_to_subscribers = true;
}
data.invite_only = JSON.stringify(invite_only);
data.history_public_to_subscribers = JSON.stringify(history_public_to_subscribers);
let stream_post_policy = parseInt($('#stream_creation_form input[name=stream-post-policy]:checked').val(), 10);
// Because the stream_post_policy field is hidden when non-administrators create streams,
// we need to set the default value here.
if (isNaN(stream_post_policy)) {
stream_post_policy = stream_data.stream_post_policy_values.everyone.code;
}
data.stream_post_policy = JSON.stringify(stream_post_policy);
2017-07-08 04:07:36 +02:00
const announce = !!page_params.notifications_stream &&
$('#announce-new-stream input').prop('checked');
data.announce = JSON.stringify(announce);
2017-07-08 04:07:36 +02:00
// TODO: We can eliminate the user_ids -> principals conversion
// once we upgrade the backend to accept user_ids.
const user_ids = get_principals();
const persons = user_ids.map(user_id => people.get_by_user_id(user_id)).filter(Boolean);
const principals = persons.map(person => person.email);
data.principals = JSON.stringify(principals);
loading.make_indicator($('#stream_creating_indicator'), {text: i18n.t('Creating stream...')});
// Subscribe yourself and possible other people to a new stream.
return channel.post({
url: "/json/users/me/subscriptions",
data: data,
success: function () {
$("#create_stream_name").val("");
$("#create_stream_description").val("");
ui_report.success(i18n.t("Stream successfully created!"), $(".stream_create_info"));
loading.destroy_indicator($('#stream_creating_indicator'));
// The rest of the work is done via the subscribe event we will get
},
error: function (xhr) {
const msg = JSON.parse(xhr.responseText).msg;
if (msg.includes('access')) {
// If we can't access the stream, we can safely assume it's
// a duplicate stream that we are not invited to.
//
// BUG: This check should be using error codes, not
// parsing the error string, so it works correctly
// with i18n. And likely we should be reporting the
// error text directly rather than turning it into
// "Error creating stream"?
stream_name_error.report_already_exists(stream_name);
stream_name_error.select();
}
ui_report.error(i18n.t("Error creating stream"), xhr, $(".stream_create_info"));
loading.destroy_indicator($('#stream_creating_indicator'));
},
});
2017-07-08 04:07:36 +02:00
}
exports.new_stream_clicked = function (stream_name) {
// this changes the tab switcher (settings/preview) which isn't necessary
// to a add new stream title.
$(".display-type #add_new_stream_title").show();
$(".display-type #stream_settings_title").hide();
$(".stream-row.active").removeClass("active");
$("#stream_settings_title, .subscriptions-container .settings, .nothing-selected").hide();
$("#stream-creation, #add_new_stream_title").show();
if (stream_name !== '') {
$('#create_stream_name').val(stream_name);
}
exports.show_new_stream_modal();
// at less than 700px we have a @media query that when you tap the
// .create_stream_button, the stream prompt slides in. However, when you
// focus the button on that page, the entire app view jumps over to
// the other tab, and the animation breaks.
// it is unclear whether this is a browser bug or "feature", however what
// is clear is that this shouldn't be touched unless you're also changing
// the mobile @media query at 700px.
if (window.innerWidth > 700) {
$('#create_stream_name').focus();
}
};
function clear_error_display() {
stream_name_error.clear_errors();
$(".stream_create_info").hide();
stream_subscription_error.clear_errors();
}
exports.show_new_stream_modal = function () {
$("#stream-creation").removeClass("hide");
$(".right .settings").hide();
const all_users = people.get_people_for_stream_create();
// Add current user on top of list
all_users.unshift(people.get_by_user_id(page_params.user_id));
const html = render_new_stream_users({
users: all_users,
streams: stream_data.get_streams_for_settings_page(),
is_admin: page_params.is_admin,
});
const container = $('#people_to_add');
container.html(html);
exports.create_handlers_for_users(container);
// Make the options default to the same each time:
// public, "announce stream" on.
$('#make-invite-only input:radio[value=public]').prop('checked', true);
if (page_params.notifications_stream) {
$('#announce-new-stream').show();
$('#announce-new-stream input').prop('disabled', false);
$('#announce-new-stream input').prop('checked', true);
} else {
$('#announce-new-stream').hide();
}
clear_error_display();
$("#stream-checkboxes label.checkbox").on('change', function (e) {
const elem = $(this);
const stream_id = parseInt(elem.attr('data-stream-id'), 10);
const checked = elem.find('input').prop('checked');
const subscriber_ids = stream_data.get_sub_by_id(stream_id).subscribers;
$('#user-checkboxes label.checkbox').each(function () {
const user_elem = $(this);
const user_id = parseInt(user_elem.attr('data-user-id'), 10);
if (subscriber_ids.has(user_id)) {
user_elem.find('input').prop('checked', checked);
}
});
e.preventDefault();
});
};
exports.create_handlers_for_users = function (container) {
// container should be $('#people_to_add')...see caller to verify
container.on('change', '#user-checkboxes input', update_announce_stream_state);
// 'Check all' and 'Uncheck all' visible users
container.on('click', '.subs_set_all_users', function (e) {
$('#user-checkboxes .checkbox').each(function (idx, li) {
if (li.style.display !== "none") {
$(li.firstElementChild).prop('checked', true);
}
});
e.preventDefault();
update_announce_stream_state();
});
container.on('click', '.subs_unset_all_users', function (e) {
$('#user-checkboxes .checkbox').each(function (idx, li) {
if (li.style.display !== "none") {
// The first checkbox is the one for ourself; this is the code path for:
// `stream_subscription_error.cant_create_stream_without_susbscribing`
if (idx === 0 && !page_params.is_admin) {
return;
}
$(li.firstElementChild).prop('checked', false);
}
});
e.preventDefault();
update_announce_stream_state();
});
container.on('click', '#copy-from-stream-expand-collapse', function (e) {
$('#stream-checkboxes').toggle();
$("#copy-from-stream-expand-collapse .toggle").toggleClass('fa-caret-right fa-caret-down');
e.preventDefault();
});
// Search People or Streams
container.on('input', '.add-user-list-filter', function (e) {
const user_list = $(".add-user-list-filter");
if (user_list === 0) {
return;
}
const search_term = user_list.expectOne().val().trim();
const search_terms = search_term.toLowerCase().split(",");
(function filter_user_checkboxes() {
const user_labels = $("#user-checkboxes label.add-user-label");
if (search_term === '') {
user_labels.css({display: 'block'});
return;
}
const users = people.get_people_for_stream_create();
const filtered_users = people.filter_people_by_search_terms(users, search_terms);
// Be careful about modifying the follow code. A naive implementation
// will work very poorly with a large user population (~1000 users).
//
// I tested using: `./manage.py populate_db --extra-users 3500`
//
// This would break the previous implementation, whereas the new
// implementation is merely sluggish.
user_labels.each(function () {
const elem = $(this);
const user_id = parseInt(elem.attr('data-user-id'), 10);
const user_checked = filtered_users.has(user_id);
const display = user_checked ? "block" : "none";
elem.css({display: display});
});
}());
e.preventDefault();
});
};
exports.set_up_handlers = function () {
const container = $('#stream-creation').expectOne();
container.on('change', '#make-invite-only input', update_announce_stream_state);
container.on("submit", "#stream_creation_form", function (e) {
e.preventDefault();
clear_error_display();
const stream_name = $.trim($("#create_stream_name").val());
const name_ok = stream_name_error.validate_for_submit(stream_name);
if (!name_ok) {
return;
}
const principals = get_principals();
if (principals.length === 0) {
stream_subscription_error.report_no_subs_to_stream();
return;
}
js: Convert a.indexOf(…) !== -1 to a.includes(…). Babel polyfills this for us for Internet Explorer. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import K from "ast-types/gen/kinds"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; recast.visit(ast, { visitBinaryExpression(path) { const { operator, left, right } = path.node; if ( n.CallExpression.check(left) && n.MemberExpression.check(left.callee) && !left.callee.computed && n.Identifier.check(left.callee.property) && left.callee.property.name === "indexOf" && left.arguments.length === 1 && checkExpression(left.arguments[0]) && ((["===", "!==", "==", "!=", ">", "<="].includes(operator) && n.UnaryExpression.check(right) && right.operator == "-" && n.Literal.check(right.argument) && right.argument.value === 1) || ([">=", "<"].includes(operator) && n.Literal.check(right) && right.value === 0)) ) { const test = b.callExpression( b.memberExpression(left.callee.object, b.identifier("includes")), [left.arguments[0]] ); path.replace( ["!==", "!=", ">", ">="].includes(operator) ? test : b.unaryExpression("!", test) ); changed = true; } this.traverse(path); }, }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 04:55:06 +01:00
if (!principals.includes(people.my_current_user_id()) && !page_params.is_admin) {
stream_subscription_error.cant_create_stream_without_susbscribing();
return;
}
if (principals.length >= 50) {
const invites_warning_modal = render_subscription_invites_warning_modal({
stream_name: stream_name,
count: principals.length,
});
$('#stream-creation').append(invites_warning_modal);
} else {
create_stream();
}
});
container.on("click", ".close-invites-warning-modal", function () {
$("#invites-warning-overlay").remove();
});
container.on("click", ".confirm-invites-warning-modal", function () {
2017-07-08 04:07:36 +02:00
create_stream();
$("#invites-warning-overlay").remove();
});
container.on("input", "#create_stream_name", function () {
const stream_name = $.trim($("#create_stream_name").val());
// This is an inexpensive check.
stream_name_error.pre_validate(stream_name);
});
container.on("mouseover", "#announce-stream-docs", function (e) {
const announce_stream_docs = $("#announce-stream-docs");
announce_stream_docs.popover({
placement: "right",
content: render_announce_stream_docs({
notifications_stream: page_params.notifications_stream}),
html: true,
trigger: "manual"});
announce_stream_docs.popover('show');
announce_stream_docs.data('popover').tip().css('z-index', 2000);
announce_stream_docs.data('popover').tip().find('.popover-content').css('margin', '9px 14px');
e.stopPropagation();
});
container.on("mouseout", "#announce-stream-docs", function (e) {
$("#announce-stream-docs").popover('hide');
e.stopPropagation();
});
// Do not allow the user to enter newline characters while typing out the
// stream's description during it's creation.
container.on("keydown", "#create_stream_description", function (e) {
if ((e.keyCode || e.which) === 13) {
e.preventDefault();
}
});
};
window.stream_create = exports;