Support locally echoing messages when sending

(imported from commit 00b5c5f9b933d119553c32cadff0f17b9f7c2879)
This commit is contained in:
Leo Franchi
2013-12-19 11:03:08 -05:00
parent 5b8e46f971
commit 00f64113e9
11 changed files with 509 additions and 51 deletions

View File

@@ -290,12 +290,14 @@ function create_message_object() {
var content = make_uploads_relative(compose.message_content()); var content = make_uploads_relative(compose.message_content());
// Changes here must also be kept in sync with echo.try_deliver_locally
var message = {client: client(), var message = {client: client(),
type: compose.composing(), type: compose.composing(),
subject: subject, subject: subject,
stream: compose.stream_name(), stream: compose.stream_name(),
private_message_recipient: compose.recipient(), private_message_recipient: compose.recipient(),
content: content}; content: content,
queue_id: page_params.event_queue_id};
if (message.type === "private") { if (message.type === "private") {
// TODO: this should be collapsed with the code in composebox_typeahead.js // TODO: this should be collapsed with the code in composebox_typeahead.js
@@ -359,7 +361,7 @@ function compose_error(error_text, bad_input) {
var send_options; var send_options;
function send_message_ajax(request, success) { function send_message_ajax(request, success, error) {
channel.post({ channel.post({
url: '/json/send_message', url: '/json/send_message',
data: request, data: request,
@@ -370,8 +372,9 @@ function send_message_ajax(request, success) {
reload.initiate({immediate: true, send_after_reload: true}); reload.initiate({immediate: true, send_after_reload: true});
return; return;
} }
var response = util.xhr_error_message("Error sending message", xhr); var response = util.xhr_error_message("Error sending message", xhr);
compose_error(response, $('#new_message_content')); error(response);
} }
}); });
} }
@@ -392,13 +395,13 @@ if (feature_flags.use_socket) {
// For debugging. The socket will eventually move out of this file anyway. // For debugging. The socket will eventually move out of this file anyway.
exports._socket = socket; exports._socket = socket;
function send_message_socket(request, success) { function send_message_socket(request, success, error) {
socket.send(request, success, function (type, resp) { socket.send(request, success, function (type, resp) {
var err_msg = "Error sending message"; var err_msg = "Error sending message";
if (type === 'response') { if (type === 'response') {
err_msg += ": " + resp.msg; err_msg += ": " + resp.msg;
} }
compose_error(err_msg, $('#new_message_content')); error(err_msg);
}); });
} }
@@ -459,6 +462,33 @@ function clear_compose_box() {
ui.resize_bottom_whitespace(); ui.resize_bottom_whitespace();
} }
exports.send_message_success = function (local_id, message_id, start_time) {
if (! feature_flags.local_echo) {
clear_compose_box();
}
process_send_time(message_id, start_time);
if (feature_flags.local_echo) {
echo.reify_message_id(local_id, message_id);
}
setTimeout(function () {
if (exports.send_times_data[message_id].received === undefined) {
blueslip.error("Restarting get_updates due to delayed receipt of sent message " + message_id);
restart_get_updates();
}
}, 5000);
};
exports.transmit_message = function (request, success, error) {
if (feature_flags.use_socket) {
send_message_socket(request, success, error);
} else {
send_message_ajax(request, success, error);
}
};
function send_message(request) { function send_message(request) {
if (request === undefined) { if (request === undefined) {
request = create_message_object(); request = create_message_object();
@@ -472,27 +502,32 @@ function send_message(request) {
} }
var start_time = new Date(); var start_time = new Date();
var local_id;
if (feature_flags.local_echo) {
local_id = echo.try_deliver_locally(request);
if (local_id !== undefined) {
// We delivered this message locally
request.local_id = local_id;
}
}
function success(data) { function success(data) {
var message_id = data.id; exports.send_message_success(local_id, data.id, start_time);
process_send_time(message_id, start_time);
if (! feature_flags.local_echo) {
clear_compose_box();
} }
setTimeout(function () { function error(response) {
if (exports.send_times_data[message_id].received === undefined) { // If we're not local echo'ing messages, or if this message was not
blueslip.error("Restarting get_updates due to delayed receipt of sent message " + message_id); // locally echoed, show error in compose box
restart_get_updates(); if (!feature_flags.local_echo || request.local_id === undefined) {
} compose_error(response, $('#new_message_content'));
}, 5000); return;
} }
if (feature_flags.use_socket) { echo.message_send_error(local_id);
send_message_socket(request, success);
} else {
send_message_ajax(request, success);
} }
exports.transmit_message(request, success, error);
if (get_updates_xhr === undefined && get_updates_timeout === undefined) { if (get_updates_xhr === undefined && get_updates_timeout === undefined) {
restart_get_updates({dont_block: true}); restart_get_updates({dont_block: true});
blueslip.error("Restarting get_updates because it was not running during send"); blueslip.error("Restarting get_updates because it was not running during send");
@@ -926,6 +961,14 @@ $(function () {
compose.start("stream", {}); compose.start("stream", {});
} }
} }
$(document).on('message_id_changed', function (event) {
if (exports.send_times_data[event.old_id] !== undefined) {
var value = exports.send_times_data[event.old_id];
delete exports.send_times_data[event.old_id];
exports.send_times_data[event.new_id] = _.extend({}, exports.send_times_data[event.old_id], value);
}
});
}); });
return exports; return exports;

View File

@@ -2,6 +2,9 @@ var echo = (function () {
var exports = {}; var exports = {};
var waiting_for_id = {};
var waiting_for_ack = {};
// Regexes that match some of our common bugdown markup // Regexes that match some of our common bugdown markup
var bugdown_re = [ var bugdown_re = [
/(?::[^:\s]+:)(?!\w)/, // Emoji /(?::[^:\s]+:)(?!\w)/, // Emoji
@@ -27,6 +30,222 @@ exports.contains_bugdown = function contains_bugdown(content) {
return markedup !== undefined; return markedup !== undefined;
}; };
exports.apply_markdown = function apply_markdown(content) {
return marked(content).trim();
};
function truncate_precision(float) {
return parseFloat(float.toFixed(3));
}
exports.try_deliver_locally = function try_deliver_locally(message_request) {
var local_id_increment = 0.01;
var next_local_id = truncate_precision(all_msg_list.last().id + local_id_increment);
if (next_local_id % 1 === 0) {
blueslip.error("Incremented local id to next integer---100 local messages queued");
}
if (exports.contains_bugdown(message_request.content)) {
return undefined;
}
// Shallow clone of message request object that is turned into something suitable
// for zulip.js:add_message
// Keep this in sync with changes to compose.create_message_object
var message = $.extend({}, message_request);
message.raw_content = message.content;
// NOTE: This will parse synchronously. We're not using the async pipeline
message.content = exports.apply_markdown(message.content);
message.content_type = 'text/html';
// Locally delivered messages cannot be unread (since we sent them), nor
// can they alert the user
message.flags = ["read"];
message.sender_email = page_params.email;
message.sender_full_name = page_params.fullname;
message.avatar_url = page_params.avatar_url;
message.timestamp = new XDate().getTime() / 1000;
message.local_id = next_local_id;
message.id = message.local_id;
waiting_for_id[message.local_id] = message;
waiting_for_ack[message.local_id] = message;
if (message.type === 'stream') {
message.display_recipient = message.stream;
} else {
// Build a display recipient with the full names of each recipient
var emails = message_request.private_message_recipient.split(',');
message.display_recipient = _.map(emails, function (email) {
email = email.trim();
var person = people_dict.get(email);
if (person !== undefined) {
return person;
}
return {email: email, full_name: email};
});
}
insert_new_messages([message]);
blueslip.debug("Generated local id " + message.local_id);
return message.local_id;
};
exports.reify_message_id = function reify_message_id(local_id, server_id) {
var message = waiting_for_id[local_id];
delete waiting_for_id[local_id];
// reify_message_id is called both on receiving a self-sent message
// from the server, and on receiving the response to the send request
// Reification is only needed the first time the server id is found
if (message === undefined) {
return;
}
blueslip.debug("Reifying ID: " + local_id + " TO " + server_id);
message.id = server_id;
delete message.local_id;
// We have the real message ID for this message
$(document).trigger($.Event('message_id_changed', {old_id: local_id, new_id: server_id}));
};
exports.process_from_server = function process_from_server(messages) {
var updated = false;
var locally_processed_ids = [];
messages = _.filter(messages, function (message) {
// In case we get the sent message before we get the send ACK, reify here
exports.reify_message_id(message.local_id, message.id);
var client_message = waiting_for_ack[message.local_id];
if (client_message !== undefined) {
if (client_message.content !== message.content) {
client_message.content = message.content;
updated = true;
}
// If a PM was sent to an out-of-realm address,
// we didn't have the full person object originally,
// so we might have to update the recipient bar and
// internal data structures
if (client_message.type === 'private') {
var reply_to = get_private_message_recipient(message, 'full_name', 'email');
if (client_message.display_reply_to !== reply_to) {
client_message.display_reply_to = reply_to;
_.each(message.display_recipient, function (person) {
if (people_dict.get(person.email).full_name !== person.full_name) {
reify_person(person);
}
});
updated = true;
}
}
locally_processed_ids.push(client_message.id);
delete waiting_for_ack[client_message.id];
return false;
}
return true;
});
if (updated) {
// TODO just rerender the message, not the whole list
home_msg_list.rerender();
if (current_msg_list === narrowed_msg_list) {
narrowed_msg_list.rerender();
}
} else {
_.each(locally_processed_ids, function (id) {
ui.show_local_message_arrived(id);
});
}
return messages;
};
exports.message_send_error = function message_send_error(local_id) {
// Error sending message, show inline
all_msg_list.get(local_id).failed_request = true;
ui.show_message_failed(local_id);
};
function resend_message(message) {
message.content = message.raw_content;
compose.transmit_message(message, function success(data) {
var message_id = data.id;
var local_id = data.local_id;
exports.reify_message_id(local_id, message_id);
// Resend succeeded, so mark as no longer failed
all_msg_list.get(message_id).failed_request = false;
ui.show_failed_message_success(message_id);
}, function error() {
blueslip.log("Manual resend of message failed");
});
}
function abort_message(message) {
// Remove in all lists in which it exists
_.each([all_msg_list, home_msg_list, current_msg_list], function (msg_list) {
msg_list.remove_and_rerender([message]);
});
}
$(function () {
function disable_markdown_regex(rules, name) {
rules[name] = {exec: function (_) {
return false;
}
};
}
// Configure the marked markdown parser for our usage
var r = new marked.Renderer();
// Disable ordered lists
// We used GFM + tables, so replace the list start regex for that ruleset
// We remove the |[\d+]\. that matches the numbering in a numbered list
marked.Lexer.rules.tables.list = /^( *)((?:\*)) [\s\S]+?(?:\n+(?=(?: *[\-*_]){3,} *(?:\n+|$))|\n{2,}(?! )(?!\1(?:\*) )\n*|\s*$)/;
// marked.Lexer.rules.tables
// Disable headings
disable_markdown_regex(marked.Lexer.rules.tables, 'heading');
disable_markdown_regex(marked.Lexer.rules.tables, 'lheading');
// Disable __strong__, all <em>
marked.InlineLexer.rules.breaks.strong = /^\*\*([\s\S]+?)\*\*(?!\*)/;
disable_markdown_regex(marked.InlineLexer.rules.breaks, 'em');
disable_markdown_regex(marked.InlineLexer.rules.breaks, 'del');
marked.setOptions({
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false,
renderer: r
});
function on_failed_action(action, callback) {
$("#main_div").on("click", "." + action + "-failed-message", function (e) {
e.stopPropagation();
popovers.hide_all();
var message_id = rows.id($(this).closest(".message_row"));
// Message should be waiting for ack and only have a local id,
// otherwise send would not have failed
var message = waiting_for_ack[message_id];
if (message === undefined) {
blueslip.warning("Got resend or retry on failure request but did not find message in ack list " + message_id);
return;
}
callback(message);
});
}
on_failed_action('remove', abort_message);
on_failed_action('refresh', resend_message);
});
return exports; return exports;
}()); }());

View File

@@ -41,6 +41,8 @@ exports.left_side_userlist = page_params.staging || _.contains(['customer7.inval
exports.show_autoscroll_forever_option = page_params.show_autoscroll_forever_option; exports.show_autoscroll_forever_option = page_params.show_autoscroll_forever_option;
// Still very beta: // Still very beta:
exports.local_echo = page_params.staging;
exports.full_width = false; //page_params.staging; exports.full_width = false; //page_params.staging;
exports.local_echo = page_params.staging; exports.local_echo = page_params.staging;

View File

@@ -24,6 +24,7 @@ function MessageList(table_name, filter, opts) {
this.num_appends = 0; this.num_appends = 0;
this.min_id_exempted_from_summaries = -1; this.min_id_exempted_from_summaries = -1;
return this; return this;
} }
@@ -514,6 +515,64 @@ MessageList.prototype = {
} }
}); });
this.view.rerender_the_whole_thing(); this.view.rerender_the_whole_thing();
},
change_message_id: function MessageList_change_message_id(old_id, new_id) {
// Update our local cache that uses the old id to the new id
function message_sort_func(a, b) {return a.id - b.id;}
function is_local_only(message) {
return message.id % 1 !== 0;
}
function next_nonlocal_message(item_list, start_index, op) {
var cur_idx = start_index;
do {
cur_idx = op(cur_idx);
} while(item_list[cur_idx] !== undefined && is_local_only(item_list[cur_idx]));
return item_list[cur_idx];
}
if (this._hash.hasOwnProperty(old_id)) {
var value = this._hash[old_id];
delete this._hash[old_id];
this._hash[new_id] = value;
}
if (this._selected_id === old_id) {
this._selected_id = new_id;
}
if (this.min_id_exempted_from_summaries === old_id) {
this.min_id_exempted_from_summaries = new_id;
}
// If this message is now out of order, re-order and re-render
var self = this;
setTimeout(function () {
var current_message = self._hash[new_id];
var index = self._items.indexOf(current_message);
if (index === -1) {
if ( !self.muting_enabled && current_msg_list === self) {
blueslip.error("Trying to re-order message but can't find message with new_id in _items!");
}
return;
}
var next = next_nonlocal_message(self._items, index, function (idx) { return idx + 1; });
var prev = next_nonlocal_message(self._items, index, function (idx) { return idx - 1; });
if ((next !== undefined && current_message.id > next.id) ||
(prev !== undefined && current_message.id < prev.id)) {
blueslip.debug("Changed message ID from server caused out-of-order list, reordering");
self._items.sort(message_sort_func);
if (self.muting_enabled) {
self._all_items.sort(message_sort_func);
}
self.view.rerender_the_whole_thing();
}
}, 0);
} }
}; };

View File

@@ -695,8 +695,32 @@ MessageListView.prototype = {
get_message: function MessageListView_get_message(id) { get_message: function MessageListView_get_message(id) {
return this.list.get(id); return this.list.get(id);
} },
change_message_id: function MessageListView_change_message_id(old_id, new_id) {
if (this._rows[old_id] !== undefined) {
var row = this._rows[old_id];
delete this._rows[old_id];
var prev_recipient_row = $(row).prev('.recipient_row');
if (prev_recipient_row.length > 0 &&
parseFloat(prev_recipient_row.attr('zid')) === old_id) {
prev_recipient_row.attr('zid', new_id);
var messages = prev_recipient_row.attr('data-messages').split();
var fixed_messages = _.map(messages, function (msgid) {
if (parseFloat(msgid) === old_id) {
return String(new_id);
}
});
prev_recipient_row.attr('data-messages', fixed_messages.join(" "));
}
row.setAttribute('zid', new_id);
row.setAttribute('id', this.table_name + new_id);
this._rows[new_id] = row;
}
}
}; };
}()); }());

View File

@@ -257,7 +257,7 @@ exports.activate = function (operators, opts) {
var defer_selecting_closest = narrowed_msg_list.empty(); var defer_selecting_closest = narrowed_msg_list.empty();
load_old_messages({ load_old_messages({
anchor: then_select_id, anchor: then_select_id.toFixed(),
num_before: 50, num_before: 50,
num_after: 50, num_after: 50,
msg_list: narrowed_msg_list, msg_list: narrowed_msg_list,

View File

@@ -700,26 +700,33 @@ function sync_message_star(message, starred) {
sync_message_flag([message], "starred", starred); sync_message_flag([message], "starred", starred);
} }
function update_message_in_all_views(message_id, callback) {
_.each([all_msg_list, home_msg_list, narrowed_msg_list], function (list) {
if (list === undefined) {
return;
}
var row = list.get_row(message_id);
if (row === undefined) {
// The row may not exist, e.g. if you do an action on a message in
// a narrowed view
return;
}
callback(row);
});
}
exports.update_starred = function (message_id, starred) { exports.update_starred = function (message_id, starred) {
// Update the message object pointed to by the various message
// lists.
var message = current_msg_list.get(message_id);
mark_message_as_read(message);
message.starred = message.starred !== true;
// Avoid a full re-render, but update the star in each message // Avoid a full re-render, but update the star in each message
// table in which it is visible. // table in which it is visible.
_.each([all_msg_list, home_msg_list, narrowed_msg_list], function (msg_list) { update_message_in_all_views(message_id, function update_row(row) {
if (msg_list === undefined) {
return;
}
var message = msg_list.get(message_id);
if (message === undefined) {
return;
}
message.starred = starred;
var row = msg_list.get_row(message.id);
if (row === undefined) {
// The row may not exist, e.g. if you star a message in the all
// messages table from a stream that isn't in your home view.
return;
}
var elt = row.find(".message_star"); var elt = row.find(".message_star");
if (starred) { if (starred) {
elt.addClass("icon-vector-star").removeClass("icon-vector-star-empty").removeClass("empty-star"); elt.addClass("icon-vector-star").removeClass("icon-vector-star-empty").removeClass("empty-star");
@@ -740,6 +747,35 @@ function toggle_star(message_id) {
sync_message_star(message, message.starred); sync_message_star(message, message.starred);
} }
var local_messages_to_show = [];
var show_message_timestamps = _.throttle(function () {
_.each(local_messages_to_show, function (message_id) {
update_message_in_all_views(message_id, function update_row(row) {
row.find('.message_time').toggleClass('notvisible', false);
});
});
local_messages_to_show = [];
}, 100);
exports.show_local_message_arrived = function (message_id) {
local_messages_to_show.push(message_id);
show_message_timestamps();
};
exports.show_message_failed = function (message_id) {
// Failed to send message, so display inline retry/cancel
update_message_in_all_views(message_id, function update_row(row) {
row.find('.message_failed').toggleClass('notvisible', false);
});
};
exports.show_failed_message_success = function (message_id) {
// Previously failed message succeeded
update_message_in_all_views(message_id, function update_row(row) {
row.find('.message_failed').toggleClass('notvisible', true);
});
};
exports.small_avatar_url = function (message) { exports.small_avatar_url = function (message) {
// Try to call this function in all places where we need 25px // Try to call this function in all places where we need 25px
// avatar images, so that the browser can help // avatar images, so that the browser can help

View File

@@ -320,18 +320,18 @@ function message_range(msg_list, start, end) {
function batched_flag_updater(flag, op) { function batched_flag_updater(flag, op) {
var queue = []; var queue = [];
var on_success;
function on_success(data, status, jqXHR) {
queue = _.filter(queue, function (message) {
return data.messages.indexOf(message) === -1;
});
}
function server_request() { function server_request() {
// Wait for server IDs before sending flags
var real_msgs = _.filter(queue, function (msg) {
return msg.local_id === undefined;
});
channel.post({ channel.post({
url: '/json/update_message_flags', url: '/json/update_message_flags',
idempotent: true, idempotent: true,
data: {messages: JSON.stringify(queue), data: {messages: JSON.stringify(real_msgs),
op: op, op: op,
flag: flag}, flag: flag},
success: on_success success: on_success
@@ -340,6 +340,20 @@ function batched_flag_updater(flag, op) {
var start = _.debounce(server_request, 1000); var start = _.debounce(server_request, 1000);
on_success = function on_success(data, status, jqXHR) {
if (data === undefined || data.messages === undefined) {
return;
}
queue = _.filter(queue, function (message) {
return data.messages.indexOf(message) === -1;
});
if (queue.length > 0) {
start();
}
};
function add(message) { function add(message) {
if (message.flags === undefined) { if (message.flags === undefined) {
message.flags = []; message.flags = [];
@@ -900,6 +914,9 @@ function get_updates_success(data) {
case 'message': case 'message':
var msg = event.message; var msg = event.message;
msg.flags = event.flags; msg.flags = event.flags;
if (event.local_message_id !== undefined) {
msg.local_id = event.local_message_id;
}
messages.push(msg); messages.push(msg);
break; break;
case 'pointer': case 'pointer':
@@ -995,6 +1012,7 @@ function get_updates_success(data) {
}); });
if (messages.length !== 0) { if (messages.length !== 0) {
messages = echo.process_from_server(messages);
insert_new_messages(messages); insert_new_messages(messages);
} }
@@ -1356,8 +1374,11 @@ function main() {
if (event.id === -1) { if (event.id === -1) {
return; return;
} }
// Additionally, don't advance the pointer server-side
// if the selected message is local-only
if (event.msg_list === home_msg_list && page_params.narrow_stream === undefined) { if (event.msg_list === home_msg_list && page_params.narrow_stream === undefined) {
if (event.id > furthest_read) { if (event.id > furthest_read &&
home_msg_list.get(event.id).local_id === undefined) {
furthest_read = event.id; furthest_read = event.id;
} }
} }
@@ -1429,6 +1450,36 @@ function main() {
} else { } else {
get_updates(); get_updates();
} }
$(document).on('message_id_changed', function (event) {
var old_id = event.old_id, new_id = event.new_id;
if (furthest_read === old_id) {
furthest_read = new_id;
}
if (get_updates_params.pointer === old_id) {
get_updates_params.pointer = new_id;
}
if (msg_metadata_cache[old_id]) {
msg_metadata_cache[new_id] = msg_metadata_cache[old_id];
delete msg_metadata_cache[old_id];
}
// This handler cannot be in the MessageList constructor, which is the logical place
// If it's there, the event handler creates a closure with a reference to the message
// list itself. When narrowing, the old narrow message list is discarded and a new one
// created, but due to the closure, the old list is not garbage collected. This also leads
// to the old list receiving the change id events, and throwing errors as it does not
// have the messages that you would expect in its internal data structures.
_.each([all_msg_list, home_msg_list, narrowed_msg_list], function (msg_list) {
if (msg_list !== undefined) {
msg_list.change_message_id(old_id, new_id);
if (msg_list.view !== undefined) {
msg_list.view.change_message_id(old_id, new_id);
}
}
});
});
} }
function install_main_scroll_handler() { function install_main_scroll_handler() {

View File

@@ -1066,6 +1066,24 @@ just a temporary hack.
color: #0088CC; color: #0088CC;
} }
.message_failed,
.message_local {
display: inline-block;
cursor: pointer;
font-size: 13px;
}
.message_failed {
font-weight: bold;
color: red;
padding: 0px 1px 0px 1px;
}
.message_failed i {
margin-left: 2px;
margin-right: 2px;
}
a.message_label_clickable:hover { a.message_label_clickable:hover {
cursor: pointer; cursor: pointer;
color: #08C; color: #08C;

View File

@@ -167,7 +167,7 @@
</div> </div>
{{/include_recipient}} {{/include_recipient}}
<div zid="{{id}}" id="{{dom_id}}" <div zid="{{id}}" id="{{dom_id}}"
class="message_row{{^is_stream}} private-message{{/is_stream}}{{#include_sender}} include-sender{{/include_sender}}{{#contains_mention}} mention{{/contains_mention}}{{#include_footer}} last_message{{/include_footer}}{{#unread}} unread{{/unread}} selectable_row"> class="message_row{{^is_stream}} private-message{{/is_stream}}{{#include_sender}} include-sender{{/include_sender}}{{#contains_mention}} mention{{/contains_mention}}{{#include_footer}} last_message{{/include_footer}}{{#unread}} unread{{/unread}} {{#if local_id}}local{{/if}} selectable_row">
<div class="unread_marker"></div> <div class="unread_marker"></div>
<div class="messagebox{{^include_sender}} prev_is_same_sender{{/include_sender}}{{^is_stream}} private-message{{/is_stream}}"> <div class="messagebox{{^include_sender}} prev_is_same_sender{{/include_sender}}{{^is_stream}} private-message{{/is_stream}}">
<div class="messagebox-border" style="box-shadow:inset 9px 0px {{background_color}};"> <div class="messagebox-border" style="box-shadow:inset 9px 0px {{background_color}};">
@@ -194,10 +194,13 @@
<span class="message_star {{#if starred}}icon-vector-star{{else}}icon-vector-star-empty empty-star{{/if}}" <span class="message_star {{#if starred}}icon-vector-star{{else}}icon-vector-star-empty empty-star{{/if}}"
title="{{#if starred}}Unstar{{else}}Star{{/if}} this message"></span> title="{{#if starred}}Unstar{{else}}Star{{/if}} this message"></span>
</div> </div>
<span class="message_time">{{timestr}}</span> <span class="message_time {{#if local_id}}notvisible{{/if}}">{{timestr}}</span>
<div class="info actions_hover"> <div class="info actions_hover">
<i class="icon-vector-chevron-down"></i> <i class="icon-vector-chevron-down"></i>
</div> </div>
<div class="message_failed {{#unless failed_request}}notvisible{{/unless}}">
<span class="failed_text">Not delivered </span><i class="icon-vector-refresh refresh-failed-message"></i><i class="icon-vector-remove-sign remove-failed-message"></i>
</div>
</div> </div>
</div> </div>
<div class="message_content">{{#unless status_message}}{{#if ../../../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}{{/unless}}</div> <div class="message_content">{{#unless status_message}}{{#if ../../../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}{{/unless}}</div>

View File

@@ -4,7 +4,7 @@
var globals = var globals =
// Third-party libraries // Third-party libraries
' $ _ jQuery Spinner Handlebars XDate zxcvbn Intl mixpanel Notification' ' $ _ jQuery Spinner Handlebars XDate zxcvbn Intl mixpanel Notification'
+ ' LazyLoad Dropbox SockJS' + ' LazyLoad Dropbox SockJS marked'
// Node-based unit tests // Node-based unit tests
+ ' module' + ' module'
@@ -41,6 +41,9 @@ var globals =
// alert_words.js // alert_words.js
+ ' alert_words' + ' alert_words'
// echo.js
+ ' echo'
// zulip.js // zulip.js
+ ' all_msg_list home_msg_list narrowed_msg_list current_msg_list get_updates_params' + ' all_msg_list home_msg_list narrowed_msg_list current_msg_list get_updates_params'
+ ' add_messages' + ' add_messages'