Extract compose_actions.js.

This module extracts these two functions that get called by
several other modules:

    start()
    cancel()

It is a little bit arbitrary which functions got pulled over
with them, but it's generally functions that would have only
been called via start/cancel.

There are two goals for splitting out this code.  The first
goal is simply to make `compose.js` have fewer responsibilities.
The second goal is to help break up circular dependencies.
The extraction of this module does more to clarify
dependencies than actually break them.  The methods start()
and cancel() had actually been shimmed in an earlier commit,
and now they no longer have a shim.

Besides start/cancel, most of the functions here are only
exported to facilitate test stubbing.  An exception is
decorate_stream_bar(), which is currently called from
ui_init.js.  We probably should move the "blur" handler out
of there, but cleaning up ui_init.js is a project for another
day.

It may seem slightly odd that this commit doesn't pull over
finish() into this module, but finish() would bring in the
whole send-message codepath.  You can think of it like this:

* compose_actions basically just populates the compose box
* compose.finish() makes the compose box do its real job,
  which is to send a message
This commit is contained in:
Steve Howell 2017-04-14 07:26:00 -07:00 committed by Tim Abbott
parent 703074215a
commit dd0c50f0df
8 changed files with 308 additions and 281 deletions

View File

@ -93,19 +93,3 @@ people.add(bob);
assert.equal(message.content, 'burrito');
}());
(function test_get_focus_area() {
assert.equal(compose._get_focus_area('private', {}), 'private_message_recipient');
assert.equal(compose._get_focus_area('private', {
private_message_recipient: 'bob@example.com'}), 'new_message_content');
assert.equal(compose._get_focus_area('stream', {}), 'stream');
assert.equal(compose._get_focus_area('stream', {stream: 'fun'}),
'subject');
assert.equal(compose._get_focus_area('stream', {stream: 'fun',
subject: 'more'}),
'new_message_content');
assert.equal(compose._get_focus_area('stream', {stream: 'fun',
subject: 'more',
trigger: 'new topic button'}),
'subject');
}());

View File

@ -6,10 +6,6 @@ set_global('document', {
},
});
add_dependencies({
util: 'js/util',
});
set_global('page_params', {
use_websockets: false,
});
@ -17,10 +13,16 @@ set_global('page_params', {
set_global('$', function () {
});
var compose = require('js/compose.js');
add_dependencies({
compose: 'js/compose',
util: 'js/util',
});
var start = compose.start;
var cancel = compose.cancel;
var compose_actions = require('js/compose_actions.js');
var start = compose_actions.start;
var cancel = compose_actions.cancel;
var get_focus_area = compose_actions._get_focus_area;
set_global('reload', {
is_in_progress: return_false,
@ -44,8 +46,8 @@ set_global('narrow_state', {
// these are shimmed in shim.js
set_global('compose_state', {
composing: compose.composing,
recipient: compose.recipient,
composing: global.compose.composing,
recipient: global.compose.recipient,
});
set_global('status_classes', 'status_classes');
@ -143,12 +145,12 @@ function assert_hidden(sel) {
}
(function test_start() {
compose.autosize_message_content = noop;
compose.expand_compose_box = noop;
compose.set_focus = noop;
compose.complete_starting_tasks = noop;
compose.blur_textarea = noop;
compose.clear_textarea = noop;
compose_actions.autosize_message_content = noop;
compose_actions.expand_compose_box = noop;
compose_actions.set_focus = noop;
compose_actions.complete_starting_tasks = noop;
compose_actions.blur_textarea = noop;
compose_actions.clear_textarea = noop;
// Start stream message
global.narrow_state.set_compose_defaults = function (opts) {
@ -187,3 +189,20 @@ function assert_hidden(sel) {
assert_visible('#compose_controls');
assert_hidden('#private-message');
}());
(function test_get_focus_area() {
assert.equal(get_focus_area('private', {}), 'private_message_recipient');
assert.equal(get_focus_area('private', {
private_message_recipient: 'bob@example.com'}), 'new_message_content');
assert.equal(get_focus_area('stream', {}), 'stream');
assert.equal(get_focus_area('stream', {stream: 'fun'}),
'subject');
assert.equal(get_focus_area('stream', {stream: 'fun',
subject: 'more'}),
'new_message_content');
assert.equal(get_focus_area('stream', {stream: 'fun',
subject: 'more',
trigger: 'new topic button'}),
'subject');
}());

View File

@ -3,6 +3,10 @@ var compose = (function () {
var exports = {};
var is_composing_message = false;
exports.set_message_type = function (msg_type) {
is_composing_message = msg_type;
};
/* Track the state of the @all warning. The user must acknowledge that they are spamming the entire
stream before the warning will go away. If they try to send before explicitly dismissing the
warning, they will get an error message too.
@ -47,43 +51,6 @@ exports.autosize_textarea = function () {
$("#new_message_content").trigger("autosize.resize");
};
// Show the compose box.
function show_box(msg_type, opts) {
if (msg_type === "stream") {
$('#private-message').hide();
$('#stream-message').show();
$("#stream_toggle").addClass("active");
$("#private_message_toggle").removeClass("active");
} else {
$('#private-message').show();
$('#stream-message').hide();
$("#stream_toggle").removeClass("active");
$("#private_message_toggle").addClass("active");
}
$("#send-status").removeClass(status_classes).hide();
$('#compose').css({visibility: "visible"});
$(".new_message_textarea").css("min-height", "3em");
exports.set_focus(msg_type, opts);
}
exports.maybe_scroll_up_selected_message = function () {
// If the compose box is obscuring the currently selected message,
// scroll up until the message is no longer occluded.
if (current_msg_list.selected_id() === -1) {
// If there's no selected message, there's no need to
// scroll the compose box to avoid it.
return;
}
var selected_row = current_msg_list.selected_row();
var cover = selected_row.offset().top + selected_row.height()
- $("#compose").offset().top;
if (cover > 0) {
message_viewport.user_initiated_animate_scroll(cover+5);
}
};
function show_all_everyone_warnings() {
var current_stream = stream_data.get_sub(compose.stream_name());
var stream_count = current_stream.subscribers.num_items();
@ -100,30 +67,20 @@ function show_all_everyone_warnings() {
user_acknowledged_all_everyone = false;
}
function clear_all_everyone_warnings() {
exports.clear_all_everyone_warnings = function () {
$("#compose-all-everyone").hide();
$("#compose-all-everyone").empty();
$("#send-status").hide();
}
function clear_invites() {
$("#compose_invite_users").hide();
$("#compose_invite_users").empty();
}
exports.clear_textarea = function () {
$("#compose").find('input[type=text], textarea').val('');
};
function clear_box() {
clear_invites();
clear_all_everyone_warnings();
exports.clear_invites = function () {
$("#compose_invite_users").hide();
$("#compose_invite_users").empty();
};
exports.reset_user_acknowledged_all_everyone_flag = function () {
user_acknowledged_all_everyone = undefined;
exports.clear_textarea();
$("#new_message_content").removeData("draft-id");
exports.autosize_textarea();
$("#send-status").hide(0);
}
};
exports.clear_preview_area = function () {
$("#new_message_content").show();
@ -133,46 +90,6 @@ exports.clear_preview_area = function () {
$("#markdown_preview").show();
};
exports.blur_textarea = function () {
$('.message_comp').find('input, textarea, button').blur();
};
function hide_box() {
exports.blur_textarea();
$('#stream-message').hide();
$('#private-message').hide();
$(".new_message_textarea").css("min-height", "");
compose_fade.clear_compose();
$('.message_comp').hide();
$("#compose_controls").show();
exports.clear_preview_area();
}
function update_lock_icon_for_stream(stream_name) {
var icon = $("#compose-lock-icon");
var streamfield = $("#stream");
if (stream_data.get_invite_only(stream_name)) {
icon.show();
streamfield.addClass("lock-padding");
} else {
icon.hide();
streamfield.removeClass("lock-padding");
}
}
// In an attempt to decrease mixing, make the composebox's
// stream bar look like what you're replying to.
// (In particular, if there's a color associated with it,
// have that color be reflected here too.)
exports.decorate_stream_bar = function (stream_name) {
var color = stream_data.get_color(stream_name);
update_lock_icon_for_stream(stream_name);
$("#stream-message .message_header_stream")
.css('background-color', color)
.removeClass(stream_color.color_classes)
.addClass(stream_color.get_color_class(color));
};
function update_fade() {
if (!is_composing_message) {
return;
@ -191,128 +108,6 @@ $(function () {
});
});
function fill_in_opts_from_current_narrowed_view(msg_type, opts) {
var default_opts = {
message_type: msg_type,
stream: '',
subject: '',
private_message_recipient: '',
trigger: 'unknown',
};
// Set default parameters based on the current narrowed view.
narrow_state.set_compose_defaults(default_opts);
opts = _.extend(default_opts, opts);
return opts;
}
function same_recipient_as_before(msg_type, opts) {
return (compose_state.composing() === msg_type) &&
((msg_type === "stream" &&
opts.stream === compose.stream_name() &&
opts.subject === compose.subject()) ||
(msg_type === "private" &&
opts.private_message_recipient === compose_state.recipient()));
}
function get_focus_area(msg_type, opts) {
// Set focus to "Topic" when narrowed to a stream+topic and "New topic" button clicked.
if (msg_type === 'stream' && opts.stream && ! opts.subject) {
return 'subject';
} else if ((msg_type === 'stream' && opts.stream)
|| (msg_type === 'private' && opts.private_message_recipient)) {
if (opts.trigger === "new topic button") {
return 'subject';
}
return 'new_message_content';
}
if (msg_type === 'stream') {
return 'stream';
}
return 'private_message_recipient';
}
// Export for testing
exports._get_focus_area = get_focus_area;
exports.set_focus = function (msg_type, opts) {
var focus_area = get_focus_area(msg_type, opts);
if (focus_area === undefined) {
return;
}
if (window.getSelection().toString() === "" ||
opts.trigger !== "message click") {
var elt = $('#' + focus_area);
elt.focus().select();
}
};
exports.autosize_message_content = function () {
$("#new_message_content").autosize();
};
exports.expand_compose_box = function () {
$("#compose_close").show();
$("#compose_controls").hide();
$('.message_comp').show();
};
exports.start = function (msg_type, opts) {
exports.autosize_message_content();
if (reload.is_in_progress()) {
return;
}
notifications.clear_compose_notifications();
exports.expand_compose_box();
opts = fill_in_opts_from_current_narrowed_view(msg_type, opts);
// If we are invoked by a compose hotkey (c or C), do not assume that we know
// what the message's topic or PM recipient should be.
if (opts.trigger === "compose_hotkey") {
opts.subject = '';
opts.private_message_recipient = '';
}
if (compose_state.composing() && !same_recipient_as_before(msg_type, opts)) {
// Clear the compose box if the existing message is to a different recipient
clear_box();
}
compose.stream_name(opts.stream);
compose.subject(opts.subject);
// Set the recipients with a space after each comma, so it looks nice.
compose_state.recipient(opts.private_message_recipient.replace(/,\s*/g, ", "));
// If the user opens the compose box, types some text, and then clicks on a
// different stream/subject, we want to keep the text in the compose box
if (opts.content !== undefined) {
compose.message_content(opts.content);
}
is_composing_message = msg_type;
// Show either stream/topic fields or "You and" field.
show_box(msg_type, opts);
exports.complete_starting_tasks(msg_type, opts);
};
exports.complete_starting_tasks = function (msg_type, opts) {
// This is sort of a kitchen sink function, and it's called only
// by compose.start() for now. Having this as a separate function
// makes testing a bit easier.
exports.maybe_scroll_up_selected_message();
ui_util.change_tab_to("#home");
compose_fade.start_compose(msg_type);
exports.decorate_stream_bar(opts.stream);
$(document).trigger($.Event('compose_started.zulip', opts));
resize.resize_bottom_whitespace();
};
exports.abort_xhr = function () {
$("#compose-send-button").removeAttr("disabled");
var xhr = $("#compose").data("filedrop_xhr");
@ -322,30 +117,6 @@ exports.abort_xhr = function () {
}
};
exports.cancel = function () {
$("#new_message_content").height(40 + "px");
if (page_params.narrow !== undefined) {
// Never close the compose box in narrow embedded windows, but
// at least clear the subject and unfade.
compose_fade.clear_compose();
if (page_params.narrow_topic !== undefined) {
compose.subject(page_params.narrow_topic);
} else {
compose.subject("");
}
return;
}
hide_box();
$("#compose_close").hide();
resize.resize_bottom_whitespace();
clear_box();
notifications.clear_compose_notifications();
abort_xhr();
is_composing_message = false;
$(document).trigger($.Event('compose_canceled.zulip'));
};
exports.empty_topic_placeholder = function () {
return i18n.t("(no topic)");
};
@ -698,7 +469,7 @@ exports.test_send_many_messages = function (stream, subject, count) {
};
exports.finish = function () {
clear_invites();
exports.clear_invites();
if (! compose.validate()) {
return false;
@ -811,7 +582,7 @@ function validate_stream_message_mentions(stream_name) {
}
} else {
// the message no longer contains @all or @everyone
clear_all_everyone_warnings();
exports.clear_all_everyone_warnings();
}
// at this point, the user has either acknowledged the warning or removed @all / @everyone
user_acknowledged_all_everyone = undefined;
@ -1006,7 +777,7 @@ $(function () {
$(event.target).parents('.compose-all-everyone').remove();
user_acknowledged_all_everyone = true;
clear_all_everyone_warnings();
exports.clear_all_everyone_warnings();
compose.finish();
});

View File

@ -0,0 +1,252 @@
var compose_actions = (function () {
var exports = {};
function update_lock_icon_for_stream(stream_name) {
var icon = $("#compose-lock-icon");
var streamfield = $("#stream");
if (stream_data.get_invite_only(stream_name)) {
icon.show();
streamfield.addClass("lock-padding");
} else {
icon.hide();
streamfield.removeClass("lock-padding");
}
}
exports.blur_textarea = function () {
$('.message_comp').find('input, textarea, button').blur();
};
function hide_box() {
exports.blur_textarea();
$('#stream-message').hide();
$('#private-message').hide();
$(".new_message_textarea").css("min-height", "");
compose_fade.clear_compose();
$('.message_comp').hide();
$("#compose_controls").show();
compose.clear_preview_area();
}
function get_focus_area(msg_type, opts) {
// Set focus to "Topic" when narrowed to a stream+topic and "New topic" button clicked.
if (msg_type === 'stream' && opts.stream && ! opts.subject) {
return 'subject';
} else if ((msg_type === 'stream' && opts.stream)
|| (msg_type === 'private' && opts.private_message_recipient)) {
if (opts.trigger === "new topic button") {
return 'subject';
}
return 'new_message_content';
}
if (msg_type === 'stream') {
return 'stream';
}
return 'private_message_recipient';
}
// Export for testing
exports._get_focus_area = get_focus_area;
exports.set_focus = function (msg_type, opts) {
var focus_area = get_focus_area(msg_type, opts);
if (focus_area === undefined) {
return;
}
if (window.getSelection().toString() === "" ||
opts.trigger !== "message click") {
var elt = $('#' + focus_area);
elt.focus().select();
}
};
// Show the compose box.
function show_box(msg_type, opts) {
if (msg_type === "stream") {
$('#private-message').hide();
$('#stream-message').show();
$("#stream_toggle").addClass("active");
$("#private_message_toggle").removeClass("active");
} else {
$('#private-message').show();
$('#stream-message').hide();
$("#stream_toggle").removeClass("active");
$("#private_message_toggle").addClass("active");
}
$("#send-status").removeClass(status_classes).hide();
$('#compose').css({visibility: "visible"});
$(".new_message_textarea").css("min-height", "3em");
exports.set_focus(msg_type, opts);
}
exports.clear_textarea = function () {
$("#compose").find('input[type=text], textarea').val('');
};
function clear_box() {
compose.clear_invites();
// TODO: Better encapsulate at-mention warnings.
compose.clear_all_everyone_warnings();
compose.reset_user_acknowledged_all_everyone_flag();
exports.clear_textarea();
$("#new_message_content").removeData("draft-id");
compose.autosize_textarea();
$("#send-status").hide(0);
}
exports.autosize_message_content = function () {
$("#new_message_content").autosize();
};
exports.expand_compose_box = function () {
$("#compose_close").show();
$("#compose_controls").hide();
$('.message_comp').show();
};
exports.complete_starting_tasks = function (msg_type, opts) {
// This is sort of a kitchen sink function, and it's called only
// by compose.start() for now. Having this as a separate function
// makes testing a bit easier.
exports.maybe_scroll_up_selected_message();
ui_util.change_tab_to("#home");
compose_fade.start_compose(msg_type);
exports.decorate_stream_bar(opts.stream);
$(document).trigger($.Event('compose_started.zulip', opts));
resize.resize_bottom_whitespace();
};
// In an attempt to decrease mixing, make the composebox's
// stream bar look like what you're replying to.
// (In particular, if there's a color associated with it,
// have that color be reflected here too.)
exports.decorate_stream_bar = function (stream_name) {
var color = stream_data.get_color(stream_name);
update_lock_icon_for_stream(stream_name);
$("#stream-message .message_header_stream")
.css('background-color', color)
.removeClass(stream_color.color_classes)
.addClass(stream_color.get_color_class(color));
};
exports.maybe_scroll_up_selected_message = function () {
// If the compose box is obscuring the currently selected message,
// scroll up until the message is no longer occluded.
if (current_msg_list.selected_id() === -1) {
// If there's no selected message, there's no need to
// scroll the compose box to avoid it.
return;
}
var selected_row = current_msg_list.selected_row();
var cover = selected_row.offset().top + selected_row.height()
- $("#compose").offset().top;
if (cover > 0) {
message_viewport.user_initiated_animate_scroll(cover+5);
}
};
function fill_in_opts_from_current_narrowed_view(msg_type, opts) {
var default_opts = {
message_type: msg_type,
stream: '',
subject: '',
private_message_recipient: '',
trigger: 'unknown',
};
// Set default parameters based on the current narrowed view.
narrow_state.set_compose_defaults(default_opts);
opts = _.extend(default_opts, opts);
return opts;
}
function same_recipient_as_before(msg_type, opts) {
return (compose_state.composing() === msg_type) &&
((msg_type === "stream" &&
opts.stream === compose.stream_name() &&
opts.subject === compose.subject()) ||
(msg_type === "private" &&
opts.private_message_recipient === compose_state.recipient()));
}
exports.start = function (msg_type, opts) {
exports.autosize_message_content();
if (reload.is_in_progress()) {
return;
}
notifications.clear_compose_notifications();
exports.expand_compose_box();
opts = fill_in_opts_from_current_narrowed_view(msg_type, opts);
// If we are invoked by a compose hotkey (c or C), do not assume that we know
// what the message's topic or PM recipient should be.
if (opts.trigger === "compose_hotkey") {
opts.subject = '';
opts.private_message_recipient = '';
}
if (compose_state.composing() && !same_recipient_as_before(msg_type, opts)) {
// Clear the compose box if the existing message is to a different recipient
clear_box();
}
compose.stream_name(opts.stream);
compose.subject(opts.subject);
// Set the recipients with a space after each comma, so it looks nice.
compose_state.recipient(opts.private_message_recipient.replace(/,\s*/g, ", "));
// If the user opens the compose box, types some text, and then clicks on a
// different stream/subject, we want to keep the text in the compose box
if (opts.content !== undefined) {
compose.message_content(opts.content);
}
compose.set_message_type(msg_type);
// Show either stream/topic fields or "You and" field.
show_box(msg_type, opts);
exports.complete_starting_tasks(msg_type, opts);
};
exports.cancel = function () {
$("#new_message_content").height(40 + "px");
if (page_params.narrow !== undefined) {
// Never close the compose box in narrow embedded windows, but
// at least clear the subject and unfade.
compose_fade.clear_compose();
if (page_params.narrow_topic !== undefined) {
compose.subject(page_params.narrow_topic);
} else {
compose.subject("");
}
return;
}
hide_box();
$("#compose_close").hide();
resize.resize_bottom_whitespace();
clear_box();
notifications.clear_compose_notifications();
compose.abort_xhr();
compose.set_message_type(false);
$(document).trigger($.Event('compose_canceled.zulip'));
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = compose_actions;
}

View File

@ -9,10 +9,6 @@ that still refer to the old name.
var narrow_state = {}; // global, should be made into module
narrow_state.set_compose_defaults = narrow.set_compose_defaults;
var compose_actions = {};
compose_actions.start = compose.start;
compose_actions.cancel = compose.cancel;
var compose_state = {};
compose_state.has_message_content = compose.has_message_content;
compose_state.recipient = compose.recipient;

View File

@ -145,7 +145,7 @@ $(function () {
$(this).removeClass("active");
});
$("#stream").on('blur', function () { compose.decorate_stream_bar(this.value); });
$("#stream").on('blur', function () { compose_actions.decorate_stream_bar(this.value); });
$(window).on('blur', function () {
$(document.body).addClass('window_blurred');

View File

@ -133,6 +133,8 @@ def find_edges_to_remove(graph, methods):
('ui', 'message_fetch'),
('ui', 'unread_ops'),
('condense', 'message_viewport'),
('compose_actions', 'compose'),
('compose_actions', 'resize'),
] # type: List[Edge]
def is_exempt(edge):
@ -167,6 +169,7 @@ def find_edges_to_remove(graph, methods):
('message_fetch', 'tutorial'),
('settings', 'subs'),
('activity', 'narrow'),
('compose', 'compose_actions'),
('compose', 'subs'),
('drafts', 'compose'),
('drafts', 'echo'),
@ -209,7 +212,8 @@ def find_edges_to_remove(graph, methods):
def cut_is_legal(edge):
# type: (Edge) -> bool
parent, child = edge
if child in ['reload', 'popovers', 'modals', 'notifications', 'server_events']:
if child in ['reload', 'popovers', 'modals', 'notifications',
'server_events', 'compose_actions']:
return True
return edge in APPROVED_CUTS

View File

@ -852,6 +852,7 @@ JS_SPECS = {
'js/fenced_code.js',
'js/echo.js',
'js/socket.js',
'js/compose_actions.js',
'js/compose.js',
'js/stream_color.js',
'js/stream_data.js',