compose: Add pills for typing in PM recipients.

@brockwhittaker wrote the original prototype for having
pills in the recipient box when users compose PMs (either
1:1 or huddle).  The prototype was test deloyed on our
main realm for several weeks.

This commit includes all the original CSS and HTML from
the prototype.

After some things changed with the codebase after the initial
test deployment, I made the following changes:

    * In prior commits I refactored out a module called
      `user_pill.js` that implemented some common functions
      against a more streamlined version of `input_pill.js`,
      and this commit largely integrates with that.

    * I made changes in a prior commit to handle Zephyr
      semantics (emails don't get validated) and tested
      this commit with zephyr.

    * I fixed a reload bug by extracting code out to
      `compose_pm_pill.js` and re-ordering some
      calls to `initialize`.

There are still two flaws related to un-pill-ified text in the
input:

    * We could be more aggressive about trying to pill-ify
      emails when you blur or tab away.

    * We only look at the pills when you send the message,
      instead of complaining about the un-pill-ified text.
      (Some folks may consider that a feature, but it's
      probably surprising to others.)
This commit is contained in:
Steve Howell
2018-03-06 09:07:55 -05:00
committed by Tim Abbott
parent 1aba722e63
commit 3a1bf04a56
19 changed files with 275 additions and 148 deletions

View File

@@ -39,6 +39,7 @@
"lightbox": false, "lightbox": false,
"input_pill": false, "input_pill": false,
"user_pill": false, "user_pill": false,
"compose_pm_pill": false,
"stream_color": false, "stream_color": false,
"people": false, "people": false,
"user_groups": false, "user_groups": false,

View File

@@ -205,6 +205,22 @@ exports.turn_off_press_enter_to_send = function () {
} }
}; };
exports.pm_recipient = {
set: function (recip) {
casper.evaluate(function (recipient) {
$("#private_message_recipient").text(recipient)
.trigger({ type: "keydown", keyCode: 13 });
}, { recipient: recip });
},
expect: function (expected_value) {
var displayed_recipients = casper.evaluate(function () {
return compose_state.recipient();
});
casper.test.assertEquals(displayed_recipients, expected_value);
},
};
// Wait for any previous send to finish, then send a message. // Wait for any previous send to finish, then send a message.
exports.then_send_message = function (type, params) { exports.then_send_message = function (type, params) {
casper.then(function () { casper.then(function () {
@@ -220,6 +236,10 @@ exports.then_send_message = function (type, params) {
} else { } else {
casper.test.assertTrue(false, "send_message got valid message type"); casper.test.assertTrue(false, "send_message got valid message type");
} }
exports.pm_recipient.set(params.recipient);
delete params.recipient;
casper.fill('form[action^="/json/messages"]', params); casper.fill('form[action^="/json/messages"]', params);
exports.turn_off_press_enter_to_send(); exports.turn_off_press_enter_to_send();
@@ -234,6 +254,9 @@ exports.then_send_message = function (type, params) {
return casper.getFormValues('form[action^="/json/messages"]').content === ''; return casper.getFormValues('form[action^="/json/messages"]').content === '';
}); });
exports.wait_for_message_actually_sent(); exports.wait_for_message_actually_sent();
casper.evaluate(function () {
compose_actions.cancel();
});
}); });
casper.then(function () { casper.then(function () {

View File

@@ -50,7 +50,7 @@ casper.then(function () {
casper.then(function () { casper.then(function () {
casper.waitUntilVisible('#private_message_recipient', function () { casper.waitUntilVisible('#private_message_recipient', function () {
common.check_form('#send_message_form', {recipient: ''}, "Recipient empty on new PM"); common.pm_recipient.expect("");
casper.click('body'); casper.click('body');
casper.page.sendEvent('keypress', 'c'); casper.page.sendEvent('keypress', 'c');
}); });
@@ -82,7 +82,7 @@ casper.then(function () {
casper.then(function () { casper.then(function () {
casper.waitUntilVisible('#private_message_recipient', function () { casper.waitUntilVisible('#private_message_recipient', function () {
common.check_form('#send_message_form', {recipient: "cordelia@zulip.com"}, "Recipient populated after PM click"); common.pm_recipient.expect("cordelia@zulip.com");
common.keypress(27); //escape common.keypress(27); //escape
casper.page.sendEvent('keypress', 'k'); casper.page.sendEvent('keypress', 'k');
@@ -151,13 +151,7 @@ casper.waitUntilVisible('#zhome', function () {
casper.then(function () { casper.then(function () {
casper.waitUntilVisible('#compose', function () { casper.waitUntilVisible('#compose', function () {
// It may be possible to get the textbox contents with CasperJS, common.pm_recipient.expect(recipients.join(','));
// but it's easier to just evaluate jQuery in page context here.
var displayed_recipients = casper.evaluate(function () {
return $('#private_message_recipient').val();
});
casper.test.assertEquals(displayed_recipients, recipients.join(', '),
'Recipients are displayed correctly in a huddle reply');
}); });
}); });

View File

@@ -63,9 +63,9 @@ casper.then(function () {
casper.page.sendEvent('keypress', "C"); casper.page.sendEvent('keypress', "C");
casper.waitUntilVisible('#private-message', function () { casper.waitUntilVisible('#private-message', function () {
casper.fill('form#send_message_form', { casper.fill('form#send_message_form', {
recipient: 'cordelia@zulip.com, hamlet@zulip.com',
content: 'Test Private Message', content: 'Test Private Message',
}, false); }, false);
common.pm_recipient.set('cordelia@zulip.com, hamlet@zulip.com');
casper.click("#compose_close"); casper.click("#compose_close");
}); });
}); });
@@ -145,9 +145,9 @@ casper.then(function () {
waitWhileDraftsVisible(function () { waitWhileDraftsVisible(function () {
casper.test.assertVisible('#private-message', 'Private Message Box Restored'); casper.test.assertVisible('#private-message', 'Private Message Box Restored');
common.check_form('form#send_message_form', { common.check_form('form#send_message_form', {
recipient: 'cordelia@zulip.com, hamlet@zulip.com',
content: 'Test Private Message', content: 'Test Private Message',
}, "Private message box filled with draft content"); }, "Private message box filled with draft content");
common.pm_recipient.expect('cordelia@zulip.com,hamlet@zulip.com');
casper.test.assertSelectorHasText('title', 'private - Zulip Dev - Zulip', 'Narrowed to huddle'); casper.test.assertSelectorHasText('title', 'private - Zulip Dev - Zulip', 'Narrowed to huddle');
}); });
}); });
@@ -178,9 +178,9 @@ casper.then(function () {
casper.then(function () { casper.then(function () {
casper.waitUntilVisible('#private-message', function () { casper.waitUntilVisible('#private-message', function () {
casper.fill('form#send_message_form', { casper.fill('form#send_message_form', {
recipient: 'cordelia@zulip.com',
content: 'Test Private Message', content: 'Test Private Message',
}, false); }, false);
common.pm_recipient.set('cordelia@zulip.com');
}); });
casper.reload(); casper.reload();
}); });

View File

@@ -56,6 +56,9 @@ zrequire('Handlebars', 'handlebars');
zrequire('stream_data'); zrequire('stream_data');
zrequire('compose_state'); zrequire('compose_state');
zrequire('people'); zrequire('people');
zrequire('input_pill');
zrequire('user_pill');
zrequire('compose_pm_pill');
zrequire('compose'); zrequire('compose');
zrequire('upload'); zrequire('upload');
@@ -83,15 +86,6 @@ people.initialize_current_user(me.user_id);
people.add(alice); people.add(alice);
people.add(bob); people.add(bob);
(function test_update_email() {
compose_state.recipient('');
assert.equal(compose.update_email(), undefined);
compose_state.recipient('bob@example.com');
compose.update_email(32, 'bob_alias@example.com');
assert.equal(compose_state.recipient(), 'bob_alias@example.com');
}());
(function test_validate_stream_message_address_info() { (function test_validate_stream_message_address_info() {
var sub = { var sub = {
stream_id: 101, stream_id: 101,
@@ -145,17 +139,42 @@ people.add(bob);
}()); }());
(function test_validate() { (function test_validate() {
$("#compose-send-button").prop('disabled', false); function initialize_pm_pill() {
$("#compose-send-button").focus(); set_global('$', global.make_zjquery());
$("#sending-indicator").hide();
$("#compose-textarea").select(noop); $("#compose-send-button").prop('disabled', false);
$("#compose-send-button").focus();
$("#sending-indicator").hide();
$("#compose-textarea").select(noop);
var pm_pill_container = $.create('fake-pm-pill-container');
$('#private_message_recipient').set_parent(pm_pill_container);
pm_pill_container.set_find_results('.input', $('#private_message_recipient'));
$('#private_message_recipient').before = noop;
compose_pm_pill.initialize();
set_global('ui_util', {
place_caret_at_end: noop,
});
$("#zephyr-mirror-error").is = noop;
$("#private_message_recipient").select(noop);
}
function add_content_to_compose_box() {
$("#compose-textarea").val('foobarfoobar');
}
initialize_pm_pill();
assert(!compose.validate()); assert(!compose.validate());
assert(!$("#sending-indicator").visible()); assert(!$("#sending-indicator").visible());
assert(!$("#compose-send-button").is_focused()); assert(!$("#compose-send-button").is_focused());
assert.equal($("#compose-send-button").prop('disabled'), false); assert.equal($("#compose-send-button").prop('disabled'), false);
assert.equal($('#compose-error-msg').html(), i18n.t('You have nothing to send!')); assert.equal($('#compose-error-msg').html(), i18n.t('You have nothing to send!'));
$("#compose-textarea").val('foobarfoobar'); add_content_to_compose_box();
var zephyr_checked = false; var zephyr_checked = false;
$("#zephyr-mirror-error").is = function () { $("#zephyr-mirror-error").is = function () {
if (!zephyr_checked) { if (!zephyr_checked) {
@@ -168,23 +187,26 @@ people.add(bob);
assert(zephyr_checked); assert(zephyr_checked);
assert.equal($('#compose-error-msg').html(), i18n.t('You need to be running Zephyr mirroring in order to send messages!')); assert.equal($('#compose-error-msg').html(), i18n.t('You need to be running Zephyr mirroring in order to send messages!'));
initialize_pm_pill();
add_content_to_compose_box();
compose_state.set_message_type('private'); compose_state.set_message_type('private');
compose_state.recipient(''); compose_state.recipient('');
$("#private_message_recipient").select(noop);
assert(!compose.validate()); assert(!compose.validate());
assert.equal($('#compose-error-msg').html(), i18n.t('Please specify at least one recipient')); assert.equal($('#compose-error-msg').html(), i18n.t('Please specify at least one valid recipient'));
initialize_pm_pill();
add_content_to_compose_box();
compose_state.recipient('foo@zulip.com'); compose_state.recipient('foo@zulip.com');
global.page_params.realm_is_zephyr_mirror_realm = true;
assert(compose.validate());
global.page_params.realm_is_zephyr_mirror_realm = false;
assert(!compose.validate()); assert(!compose.validate());
assert.equal($('#compose-error-msg').html(), i18n.t('The recipient foo@zulip.com is not valid', {}));
assert.equal($('#compose-error-msg').html(), i18n.t('Please specify at least one valid recipient', {}));
compose_state.recipient('foo@zulip.com,alice@zulip.com'); compose_state.recipient('foo@zulip.com,alice@zulip.com');
assert(!compose.validate()); assert(!compose.validate());
assert.equal($('#compose-error-msg').html(), i18n.t('The recipients foo@zulip.com,alice@zulip.com are not valid', {}));
assert.equal($('#compose-error-msg').html(), i18n.t('Please specify at least one valid recipient', {}));
people.add_in_realm(bob); people.add_in_realm(bob);
compose_state.recipient('bob@example.com'); compose_state.recipient('bob@example.com');
@@ -1339,7 +1361,7 @@ function test_raw_file_drop(raw_drop_func) {
}()); }());
(function test_set_focused_recipient() { (function test_create_message_object() {
var sub = { var sub = {
stream_id: 101, stream_id: 101,
name: 'social', name: 'social',
@@ -1351,7 +1373,6 @@ function test_raw_file_drop(raw_drop_func) {
'#stream': 'social', '#stream': 'social',
'#subject': 'lunch', '#subject': 'lunch',
'#compose-textarea': 'burrito', '#compose-textarea': 'burrito',
'#private_message_recipient': 'alice@example.com, bob@example.com',
}; };
global.$ = function (selector) { global.$ = function (selector) {
@@ -1379,6 +1400,7 @@ function test_raw_file_drop(raw_drop_func) {
global.compose_state.get_message_type = function () { global.compose_state.get_message_type = function () {
return 'private'; return 'private';
}; };
compose_state.recipient('alice@example.com, bob@example.com');
message = compose.create_message_object(); message = compose.create_message_object();
assert.deepEqual(message.to, ['alice@example.com', 'bob@example.com']); assert.deepEqual(message.to, ['alice@example.com', 'bob@example.com']);
assert.equal(message.to_user_ids, '31,32'); assert.equal(message.to_user_ids, '31,32');

View File

@@ -16,6 +16,9 @@ set_global('$', function () {
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
set_global('compose_pm_pill', {
});
zrequire('people'); zrequire('people');
zrequire('compose_ui'); zrequire('compose_ui');
zrequire('compose'); zrequire('compose');
@@ -31,6 +34,18 @@ var reply_with_mention = compose_actions.reply_with_mention;
var compose_state = global.compose_state; var compose_state = global.compose_state;
compose_state.recipient = (function () {
var recipient;
return function (arg) {
if (arg === undefined) {
return recipient;
}
recipient = arg;
};
}());
set_global('reload', { set_global('reload', {
is_in_progress: return_false, is_in_progress: return_false,
}); });
@@ -129,14 +144,21 @@ function assert_hidden(sel) {
assert_hidden('#stream-message'); assert_hidden('#stream-message');
assert_visible('#private-message'); assert_visible('#private-message');
assert.equal($('#private_message_recipient').val(), 'foo@example.com'); assert.equal(compose_state.recipient(), 'foo@example.com');
assert.equal($('#compose-textarea').val(), 'hello'); assert.equal($('#compose-textarea').val(), 'hello');
assert.equal(compose_state.get_message_type(), 'private'); assert.equal(compose_state.get_message_type(), 'private');
assert(compose_state.composing()); assert(compose_state.composing());
// Cancel compose. // Cancel compose.
var pill_cleared;
compose_pm_pill.clear = function () {
pill_cleared = true;
};
assert_hidden('#compose_controls'); assert_hidden('#compose_controls');
cancel(); cancel();
assert(pill_cleared);
assert_visible('#compose_controls'); assert_visible('#compose_controls');
assert_hidden('#private-message'); assert_hidden('#private-message');
assert(!compose_state.composing()); assert(!compose_state.composing());
@@ -162,7 +184,7 @@ function assert_hidden(sel) {
}; };
respond_to_message(opts); respond_to_message(opts);
assert.equal($('#private_message_recipient').val(), 'alice@example.com'); assert.equal(compose_state.recipient(), 'alice@example.com');
// Test stream // Test stream
msg = { msg = {

View File

@@ -8,9 +8,12 @@ zrequire('typeahead_helper');
zrequire('people'); zrequire('people');
zrequire('user_groups'); zrequire('user_groups');
zrequire('stream_data'); zrequire('stream_data');
zrequire('user_pill');
zrequire('compose_pm_pill');
zrequire('composebox_typeahead'); zrequire('composebox_typeahead');
var ct = composebox_typeahead; var ct = composebox_typeahead;
var noop = function () {};
var emoji_stadium = { var emoji_stadium = {
emoji_name: 'stadium', emoji_name: 'stadium',
@@ -64,6 +67,9 @@ set_global('$', global.make_zjquery());
set_global('page_params', {}); set_global('page_params', {});
set_global('channel', {}); set_global('channel', {});
set_global('compose', {
finish: noop,
});
set_global('emoji', { set_global('emoji', {
active_realm_emojis: {}, active_realm_emojis: {},
@@ -133,6 +139,11 @@ var backend = {
global.user_groups.add(hamletcharacters); global.user_groups.add(hamletcharacters);
global.user_groups.add(backend); global.user_groups.add(backend);
user_pill.get_user_ids = function () {
return [];
};
(function test_add_topic() { (function test_add_topic() {
ct.add_topic('Denmark', 'civil fears'); ct.add_topic('Denmark', 'civil fears');
ct.add_topic('devel', 'fading'); ct.add_topic('devel', 'fading');
@@ -451,17 +462,6 @@ global.user_groups.add(backend);
assert.equal(options.matcher(othello), true); assert.equal(options.matcher(othello), true);
assert.equal(options.matcher(cordelia), true); assert.equal(options.matcher(cordelia), true);
// Othello is already filled in, now typeahead makes suggestions for
// the value after the comma.
options.query = 'othello@zulip.com, cor';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), true);
// No suggestions are made if the query is just a comma.
options.query = ',';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false);
options.query = 'bender'; // Doesn't exist options.query = 'bender'; // Doesn't exist
assert.equal(options.matcher(othello), false); assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false); assert.equal(options.matcher(cordelia), false);
@@ -472,7 +472,8 @@ global.user_groups.add(backend);
assert.equal(options.matcher(othello), false); assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false); assert.equal(options.matcher(cordelia), false);
options.query = 'othello@zulip.com,, , cord'; // options.query = 'othello@zulip.com,, , cord';
options.query = 'cord';
assert.equal(options.matcher(othello), false); assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), true); assert.equal(options.matcher(cordelia), true);
@@ -503,27 +504,30 @@ global.user_groups.add(backend);
expected_value = []; expected_value = [];
assert.deepEqual(actual_value, expected_value); assert.deepEqual(actual_value, expected_value);
var event = {
target: '#doesnotmatter',
};
var appended_name;
compose_pm_pill.set_from_typeahead = function (item) {
appended_name = item.full_name;
};
// options.updater() // options.updater()
options.query = 'othello'; options.query = 'othello';
actual_value = options.updater(othello); options.updater(othello, event);
expected_value = 'othello@zulip.com, '; assert.equal(appended_name, 'Othello, the Moor of Venice');
assert.equal(actual_value, expected_value);
options.query = 'othello@zulip.com, cor'; options.query = 'othello@zulip.com, cor';
actual_value = options.updater(cordelia); actual_value = options.updater(cordelia, event);
expected_value = 'othello@zulip.com, cordelia@zulip.com, '; assert.equal(appended_name, 'Cordelia Lear');
assert.equal(actual_value, expected_value);
var click_event = { type: 'click' }; var click_event = { type: 'click', target: '#doesnotmatter' };
options.query = 'othello'; options.query = 'othello';
// Focus lost (caused by the click event in the typeahead list) // Focus lost (caused by the click event in the typeahead list)
$('#private_message_recipient').blur(); $('#private_message_recipient').blur();
actual_value = options.updater(othello, click_event); actual_value = options.updater(othello, click_event);
expected_value = 'othello@zulip.com, '; assert.equal(appended_name, 'Othello, the Moor of Venice');
assert.equal(actual_value, expected_value);
// Check that after the click event #private_message_recipient is
// focused.
assert.equal($('#private_message_recipient').is_focused(), true);
pm_recipient_typeahead_called = true; pm_recipient_typeahead_called = true;
}; };
@@ -681,8 +685,6 @@ global.user_groups.add(backend);
page_params.enter_sends = false; // We manually specify it the first page_params.enter_sends = false; // We manually specify it the first
// time because the click_func // time because the click_func
// doesn't exist yet. // doesn't exist yet.
var noop = function () {};
$("#stream").select(noop); $("#stream").select(noop);
$("#subject").select(noop); $("#subject").select(noop);
$("#private_message_recipient").select(noop); $("#private_message_recipient").select(noop);
@@ -743,11 +745,10 @@ global.user_groups.add(backend);
page_params.enter_sends = false; page_params.enter_sends = false;
event.metaKey = true; event.metaKey = true;
var compose_finish_called = false; var compose_finish_called = false;
set_global('compose', { compose.finish = function () {
finish: function () { compose_finish_called = true;
compose_finish_called = true; };
},
});
$('form#send_message_form').keydown(event); $('form#send_message_form').keydown(event);
assert(compose_finish_called); assert(compose_finish_called);
event.metaKey = false; event.metaKey = false;

View File

@@ -676,6 +676,7 @@ initialize();
assert(people.is_valid_email_for_compose('bot@example.com')); assert(people.is_valid_email_for_compose('bot@example.com'));
assert(people.is_valid_email_for_compose('alice@example.com')); assert(people.is_valid_email_for_compose('alice@example.com'));
assert(!people.is_valid_email_for_compose('retiree@example.com')); assert(!people.is_valid_email_for_compose('retiree@example.com'));
assert(!people.is_valid_email_for_compose('totally-bogus-username@example.com'));
assert(people.is_my_user_id(42)); assert(people.is_my_user_id(42));
var fetched_retiree = people.get_person_from_user_id(15); var fetched_retiree = people.get_person_from_user_id(15);

View File

@@ -567,8 +567,8 @@ function validate_stream_message() {
// The function checks whether the recipients are users of the realm or cross realm users (bots // The function checks whether the recipients are users of the realm or cross realm users (bots
// for now) // for now)
function validate_private_message() { function validate_private_message() {
if (compose_state.recipient() === "") { if (compose_state.recipient().length === 0) {
compose_error(i18n.t("Please specify at least one recipient"), $("#private_message_recipient")); compose_error(i18n.t("Please specify at least one valid recipient"), $("#private_message_recipient"));
return false; return false;
} else if (page_params.realm_is_zephyr_mirror_realm) { } else if (page_params.realm_is_zephyr_mirror_realm) {
// For Zephyr mirroring realms, the frontend doesn't know which users exist // For Zephyr mirroring realms, the frontend doesn't know which users exist

View File

@@ -244,6 +244,7 @@ exports.cancel = function () {
notifications.clear_compose_notifications(); notifications.clear_compose_notifications();
compose.abort_xhr(); compose.abort_xhr();
compose_state.set_message_type(false); compose_state.set_message_type(false);
compose_pm_pill.clear();
$(document).trigger($.Event('compose_canceled.zulip')); $(document).trigger($.Event('compose_canceled.zulip'));
}; };

View File

@@ -0,0 +1,62 @@
var compose_pm_pill = (function () {
var exports = {};
exports.initialize_pill = function () {
var pill;
var container = $("#private_message_recipient").parent();
pill = input_pill.create({
container: container,
create_item_from_text: user_pill.create_item_from_email,
get_text_from_item: user_pill.get_email_from_item,
});
return pill;
};
exports.initialize = function () {
exports.my_pill = exports.initialize_pill();
};
exports.clear = function () {
exports.my_pill.clear();
};
exports.set_from_typeahead = function (person) {
// We expect person to be an object returned from people.js.
user_pill.append_person({
pill_widget: exports.my_pill,
person: person,
});
};
exports.set_from_emails = function (value) {
// value is something like "alice@example.com,bob@example.com"
exports.clear();
exports.my_pill.appendValue(value);
};
exports.get_user_ids = function () {
return user_pill.get_user_ids(exports.my_pill);
};
exports.get_emails = function () {
// return something like "alice@example.com,bob@example.com"
var user_ids = exports.get_user_ids();
var emails = user_ids.map(function (id) {
return people.get_person_from_user_id(id).email;
}).join(",");
return emails;
};
exports.get_typeahead_items = function () {
return user_pill.typeahead_source(exports.my_pill);
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = compose_pm_pill;
}

View File

@@ -45,7 +45,13 @@ exports.subject = get_or_set('subject');
// We can't trim leading whitespace in `compose_textarea` because // We can't trim leading whitespace in `compose_textarea` because
// of the indented syntax for multi-line code blocks. // of the indented syntax for multi-line code blocks.
exports.message_content = get_or_set('compose-textarea', true); exports.message_content = get_or_set('compose-textarea', true);
exports.recipient = get_or_set('private_message_recipient'); exports.recipient = function (value) {
if (typeof value === "string") {
compose_pm_pill.set_from_emails(value);
} else {
return compose_pm_pill.get_emails();
}
};
exports.has_message_content = function () { exports.has_message_content = function () {
return exports.message_content() !== ""; return exports.message_content() !== "";

View File

@@ -39,11 +39,6 @@ exports.topics_seen_for = function (stream) {
return []; return [];
}; };
function get_last_recipient_in_pm(query_string) {
var recipients = util.extract_pm_recipients(query_string);
return recipients[recipients.length-1];
}
function query_matches_language(query, lang) { function query_matches_language(query, lang) {
query = query.toLowerCase(); query = query.toLowerCase();
return lang.indexOf(query) !== -1; return lang.indexOf(query) !== -1;
@@ -638,7 +633,7 @@ exports.initialize = function () {
}); });
$("#private_message_recipient").typeahead({ $("#private_message_recipient").typeahead({
source: people.get_realm_persons, // This is a function. source: compose_pm_pill.get_typeahead_items,
items: 5, items: 5,
dropup: true, dropup: true,
fixed: true, fixed: true,
@@ -646,34 +641,15 @@ exports.initialize = function () {
return typeahead_helper.render_person(item); return typeahead_helper.render_person(item);
}, },
matcher: function (item) { matcher: function (item) {
var current_recipient = get_last_recipient_in_pm(this.query); return query_matches_person(this.query, item);
// If you type just a comma, there won't be any recipients.
if (!current_recipient) {
return false;
}
var recipients = util.extract_pm_recipients(this.query);
if (recipients.indexOf(item.email) > -1) {
return false;
}
return query_matches_person(current_recipient, item);
}, },
sorter: function (matches) { sorter: function (matches) {
// var current_stream = compose_state.stream_name(); // var current_stream = compose_state.stream_name();
return typeahead_helper.sort_recipientbox_typeahead( return typeahead_helper.sort_recipientbox_typeahead(
this.query, matches, ""); this.query, matches, "");
}, },
updater: function (item, event) { updater: function (item) {
var previous_recipients = typeahead_helper.get_cleaned_pm_recipients(this.query); compose_pm_pill.set_from_typeahead(item);
previous_recipients.pop();
previous_recipients = previous_recipients.join(", ");
if (previous_recipients.length !== 0) {
previous_recipients += ", ";
}
if (event && event.type === 'click') {
ui_util.focus_on('private_message_recipient');
}
return previous_recipients + item.email + ", ";
}, },
stopAdvance: true, // Do not advance to the next field on a tab or enter stopAdvance: true, // Do not advance to the next field on a tab or enter
}); });

View File

@@ -252,16 +252,18 @@ $(function () {
} }
// initialize other stuff // initialize other stuff
reload.initialize();
server_events.initialize(); server_events.initialize();
people.initialize(); people.initialize();
compose_pm_pill.initialize();
reload.initialize();
user_groups.initialize(); user_groups.initialize();
unread.initialize(); unread.initialize();
bot_data.initialize(); // Must happen after people.initialize() bot_data.initialize(); // Must happen after people.initialize()
message_fetch.initialize(); message_fetch.initialize();
emoji.initialize(); emoji.initialize();
markdown.initialize(); // Must happen after emoji.initialize() markdown.initialize(); // Must happen after emoji.initialize()
composebox_typeahead.initialize(); compose.initialize();
composebox_typeahead.initialize(); // Must happen after compose.initialize()
search.initialize(); search.initialize();
tutorial.initialize(); tutorial.initialize();
notifications.initialize(); notifications.initialize();
@@ -277,7 +279,6 @@ $(function () {
stream_list.initialize(); stream_list.initialize();
drafts.initialize(); drafts.initialize();
sent_messages.initialize(); sent_messages.initialize();
compose.initialize();
hotspots.initialize(); hotspots.initialize();
ui.initialize(); ui.initialize();
panels.initialize(); panels.initialize();

View File

@@ -79,34 +79,23 @@
} }
.compose_table .pm_recipient { .compose_table .pm_recipient {
margin: 0px 20px 0px 10px;
display: flex;
}
.compose_table #private-message .to_text {
width: 65px;
vertical-align: top;
font-weight: 600;
}
.compose_table #private-message .to_text span {
display: flex;
align-items: center;
position: relative; position: relative;
margin-right: 30px; top: -1px;
height: 25px;
}
.compose_table #private-message .you_text {
position: absolute;
height: 25px;
line-height: 25px;
padding-top: 0px;
padding-bottom: 0px;
width: 4em;
background: hsl(0, 0%, 27%);
color: hsl(0, 0%, 100%);
}
.compose_table .pm_recipient #private_message_recipient {
margin-left: 4em;
border-left: none;
border-radius: 0px 3px 3px 0px;
padding-top: 0px;
padding-bottom: 0px;
height: 23px;
line-height: 23px;
}
.compose_table #private-message .right_part {
padding-right: 4em;
} }
.compose_table #compose-lock-icon { .compose_table #compose-lock-icon {

View File

@@ -83,7 +83,8 @@ body.dark-mode input[type="email"],
body.dark-mode input[type="password"], body.dark-mode input[type="password"],
body.dark-mode textarea, body.dark-mode textarea,
body.dark-mode .new-style .tab-switcher .ind-tab:not(.selected), body.dark-mode .new-style .tab-switcher .ind-tab:not(.selected),
body.dark-mode select { body.dark-mode select,
body.dark-mode .pill-container {
background-color: hsla(0, 0%, 0%, 0.2); background-color: hsla(0, 0%, 0%, 0.2);
border-color: hsla(0, 0%, 0%, 0.6); border-color: hsla(0, 0%, 0%, 0.6);
color: inherit; color: inherit;
@@ -94,6 +95,19 @@ body.dark-mode select option {
color: hsl(236, 33%, 90%); color: hsl(236, 33%, 90%);
} }
body.dark-mode .pm_recipient .pill-container .pill {
color: inherit;
border: 1px solid hsla(0, 0%, 0%, 0.50);
background: hsla(0, 0%, 0%, 0.25);
font-weight: 600;
}
body.dark-mode .pm_recipient .pill-container .pill:focus {
color: #fff;
border: 1px solid hsla(176, 78%, 28%, 0.6);
background: hsla(176, 49%, 42%, 0.4);
}
body.dark-mode .new-style .button.no-style { body.dark-mode .new-style .button.no-style {
background-color: transparent; background-color: transparent;
} }

View File

@@ -2,7 +2,7 @@
display: inline-block; display: inline-block;
padding: 2px; padding: 2px;
border: 1px solid hsl(0, 0%, 86%); border: 1px solid hsla(0, 0%, 0%, 0.15);
border-radius: 4px; border-radius: 4px;
font-size: 0.9em; font-size: 0.9em;
@@ -14,7 +14,7 @@
position: relative; position: relative;
display: inline-block; display: inline-block;
padding: 0px 18px 0px 2px; padding: 0px 18px 0px 3px;
margin: 1px 1px; margin: 1px 1px;
color: hsl(177, 38%, 54%); color: hsl(177, 38%, 54%);
@@ -64,29 +64,41 @@
} }
.pm_recipient .pill-container { .pm_recipient .pill-container {
position: absolute; position: relative;
left: 56px;
width: 100%; width: 100%;
height: 24px; padding: 0px 2px;
border-top-left-radius: 0px; /* this is because the pills have a margin-bottom of 2px, so we want to negate
border-bottom-left-radius: 0px; the height of the bottom row. */
margin: -1px 0px;
border: none;
} }
.pm_recipient .pill-container .pill { .pm_recipient .pill-container .pill {
color: hsl(0, 0%, 100%); color: inherit;
border: 1px solid #36a29b; border: 1px solid hsla(0, 0%, 0%, 0.15);
background: hsl(170, 47%, 53%); background: hsla(0, 0%, 0%, 0.07);
font-weight: 600;
} }
.pm_recipient .pill-container .pill:focus { .pm_recipient .pill-container .pill:focus {
color: #fff;
border: 1px solid #108179; border: 1px solid #108179;
background: hsl(176, 49%, 42%); background: hsl(176, 49%, 42%);
} }
.pm_recipient .pill-container .input { .pm_recipient .pill-container .input {
height: 12px; height: 20px;
}
.pm_recipient .pill-container .input:empty::before {
content: attr(data-no-recipients-text);
opacity: 0.5;
}
.pm_recipient .pill-container .pill + .input:empty::before {
content: attr(data-some-recipients-text);
opacity: 0.5;
} }
@keyframes shake { @keyframes shake {

View File

@@ -78,21 +78,22 @@
</td> </td>
</tr> </tr>
<tr id="private-message"> <tr id="private-message">
<td class="message_header_colorblock message_header_private_message message_header left_part"> <td class="to_text">
<span>{{ _('To') }}:</span>
</td> </td>
<td class="right_part"> <td class="right_part">
<div class="pm_recipient"> <div class="pm_recipient">
<span class="you_text">{{ _('You and') }}</span> <div class="pill-container" data-before="{{ _('You and') }}">
<input type="text" class="recipient_box" name="recipient" id="private_message_recipient" <div class="input" contenteditable="true" id="private_message_recipient" name="recipient"
value="" placeholder="{{ _('one or more people...') }}" autocomplete="off" tabindex="0"/> data-no-recipients-text="{{ _('Add one or more users') }}" data-some-recipients-text="{{ _('Add another user...') }}"></div>
</div>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td class="messagebox" colspan="2"> <td class="messagebox" colspan="2">
<textarea class="new_message_textarea" name="content" id='compose-textarea' <textarea class="new_message_textarea" name="content" id='compose-textarea'
value="" placeholder="{{ _('Compose your message here...') }}" tabindex="0" maxlength="10000" aria-label="{{ _('Compose your message here...') }}"></textarea> value="" placeholder="{{ _('Compose your message here') }}" tabindex="0" maxlength="10000" aria-label="{{ _('Compose your message here...') }}"></textarea>
<div class="scrolling_list" id="preview_message_area" style="display:none;"> <div class="scrolling_list" id="preview_message_area" style="display:none;">
<div id="markdown_preview_spinner"></div> <div id="markdown_preview_spinner"></div>
<div id="preview_content"></div> <div id="preview_content"></div>

View File

@@ -1005,6 +1005,7 @@ JS_SPECS = {
'js/drafts.js', 'js/drafts.js',
'js/input_pill.js', 'js/input_pill.js',
'js/user_pill.js', 'js/user_pill.js',
'js/compose_pm_pill.js',
'js/channel.js', 'js/channel.js',
'js/setup.js', 'js/setup.js',
'js/unread_ui.js', 'js/unread_ui.js',