Add user interface frontend for editing messages.

The only known outstanding bug with this is that it doesn't properly
handle the updating of a message's highlighting/presence in a narrowed
view (e.g. in theory, a message should disappear if it is edited such
that its subject doesn't match your narrow or it no longer matches
your search).  I think I'll just open a trac ticket about that once
this is merged, since it's a little hairy to deal with and kinda a
marginal use case.

Also it's not pretty, but that should be easy to tweak once we get the
framework merged.

Conflicts:
	tools/jslint/check-all.js

(imported from commit 2d0e3a440bcd885546bd8e28aff97bf379649950)
This commit is contained in:
Tim Abbott
2013-05-14 18:22:16 -04:00
committed by Leo Franchi
parent d467a93877
commit 2bdecd130a
11 changed files with 171 additions and 3 deletions

View File

@@ -288,6 +288,7 @@ PIPELINE_JS = {
'js/notifications_bar.js',
'js/compose.js',
'js/subs.js',
'js/message_edit.js',
'js/ui.js',
'js/typeahead_helper.js',
'js/search.js',

View File

@@ -113,6 +113,7 @@ urlpatterns += patterns('zephyr.views',
url(r'^json/get_bots$', 'json_get_bots'),
url(r'^json/update_onboarding_steps$', 'json_update_onboarding_steps'),
url(r'^json/update_message$', 'json_update_message'),
url(r'^json/fetch_raw_message$', 'json_fetch_raw_message'),
# These are json format views used by the API. They require an API key.
url(r'^api/v1/get_profile$', 'api_get_profile'),

View File

@@ -18,7 +18,7 @@ var globals =
+ ' compose rows hotkeys narrow reload notifications_bar search subs'
+ ' composebox_typeahead typeahead_helper notifications hashchange'
+ ' invite ui util activity timerender MessageList blueslip stream_list'
+ ' onboarding tab_bar'
+ ' onboarding message_edit tab_bar'
// colorspace.js
+ ' colorspace'

View File

@@ -0,0 +1,89 @@
var message_edit = (function () {
var exports = {};
var currently_editing_messages = {};
exports.save = function (row) {
var msg_list = current_msg_list;
var message = current_msg_list.get(rows.id(row));
var new_subject = row.find(".message_edit_subject").val();
var new_content = row.find(".message_edit_content").val();
var request = {message_id: message.id};
if (new_subject !== message.subject) {
request.subject = new_subject;
}
if (new_content !== message.raw_content) {
request.content = new_content;
}
if (request.subject === undefined &&
request.content === undefined) {
// If they didn't change anything, just cancel it.
return message_edit.cancel(row);
}
$.ajax({
type: 'POST',
url: '/json/update_message',
data: request,
dataType: 'json',
success: function (data) {
if (msg_list === current_msg_list) {
message_edit.cancel(row);
}
}
});
// The message will automatically get replaced when it arrives.
};
function edit_message (row, raw_content) {
var message = current_msg_list.get(rows.id(row));
var edit_row = row.find(".message_edit");
var form = $(templates.render('message_edit_form',
{is_stream: message.is_stream,
subject: message.subject,
content: raw_content}));
var edit_obj = {form: form, raw_content: raw_content};
current_msg_list.show_edit_message(row, edit_obj);
currently_editing_messages[message.id] = edit_obj;
}
exports.start = function (row) {
var message = current_msg_list.get(rows.id(row));
var msg_list = current_msg_list;
$.ajax({
type: 'POST',
url: '/json/fetch_raw_message',
data: {message_id: message.id},
dataType: 'json',
success: function (data) {
if (current_msg_list === msg_list) {
message.raw_content = data.raw_content;
edit_message(row, data.raw_content);
}
}
});
};
exports.cancel = function (row) {
var message = current_msg_list.get(rows.id(row));
delete currently_editing_messages[message.id];
current_msg_list.hide_edit_message(row);
};
exports.maybe_show_edit = function(row, id) {
if (currently_editing_messages[id] !== undefined){
current_msg_list.show_edit_message(row, currently_editing_messages[id]);
}
};
$(document).on('narrow_deactivated.zephyr', function (event) {
$.each(currently_editing_messages, function(idx, elem) {
if (current_msg_list.get(idx) !== undefined) {
var row = rows.get(idx, current_msg_list.table_name);
current_msg_list.show_edit_message(row, elem);
}
});
});
return exports;
}());

View File

@@ -402,6 +402,14 @@ MessageList.prototype = {
}});
}
$.each(rendered_elems, function(idx, elem) {
var row = $(elem);
if (! row.hasClass('message_row')) {
return;
}
var id = rows.id(row);
message_edit.maybe_show_edit(row, id);
});
$.each(rendered_elems, ui.process_condensing);
// Re-add the fading of messages that is lost when we re-render.
@@ -524,6 +532,17 @@ MessageList.prototype = {
this._render_win_end), 'bottom');
},
show_edit_message: function MessageList_show_edit_message(row, edit_obj) {
row.find(".message_edit_form").empty().append(edit_obj.form);
row.find(".message_content").hide();
row.find(".message_edit").show();
},
hide_edit_message: function MessageList_hide_edit_message(row) {
row.find(".message_content").show();
row.find(".message_edit").hide();
},
rerender: function MessageList_rerender() {
// We need to clear the rendering state, rather than just
// doing _clear_table, since we want to potentially recollapse

View File

@@ -535,8 +535,11 @@ function show_actions_popover(element, id) {
timerender.set_full_datetime(current_msg_list.get(id),
elt.closest(".message_row").find(".message_time"));
var message = current_msg_list.get(id);
var can_edit = message.sender_email.toLowerCase() === page_params.email.toLowerCase();
var args = {
message: current_msg_list.get(id),
message: message,
can_edit_message: can_edit,
narrowed: narrow.active()
};
@@ -1055,7 +1058,7 @@ $(function () {
$("#main_div").on("click", ".messagebox", function (e) {
var target = $(e.target);
if (target.is("a") || target.is("img.message_inline_image") || target.is("img.twitter-avatar") ||
target.is("div.message_length_controller")) {
target.is("div.message_length_controller") || target.is("textarea") || target.is("input")) {
// If this click came from a hyperlink, don't trigger the
// reply action. The simple way of doing this is simply
// to call e.stopPropagation() from within the link's
@@ -1396,6 +1399,23 @@ $(function () {
e.stopPropagation();
});
$('body').on('click', '.popover_edit_message', function (e) {
var msgid = $(e.currentTarget).data('msgid');
var row = rows.get(msgid, current_msg_list.table_name);
ui.hide_actions_popover();
message_edit.start(row);
e.stopPropagation();
});
$("body").on("click", ".message_edit_save", function (e) {
var row = $(this).closest(".message_row");
message_edit.save(row);
e.stopPropagation();
});
$("body").on("click", ".message_edit_cancel", function (e) {
var row = $(this).closest(".message_row");
message_edit.cancel(row);
e.stopPropagation();
});
$('body').on('click', '.toggle_home', function (e) {

View File

@@ -1435,3 +1435,7 @@ li.expanded_subject {
text-align: center;
padding-bottom: 10px;
}
.message_edit {
display: none;
}

View File

@@ -61,6 +61,14 @@
</li>
{{/if}}
{{#if can_edit_message}}
<li>
<a class="popover_edit_message" data-msgid="{{message.id}}">
<i class="icon-pencil"></i> Edit this message
</a>
</li>
{{/if}}
<li>
<a class="popover_toggle_collapse" data-msgid="{{message.id}}">
<i class="{{#if message.collapsed}}icon-plus{{else}}icon-minus{{/if}}"></i>

View File

@@ -78,6 +78,10 @@
{{/include_sender}}
</div>
<div class="message_content">{{#if ../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}</div>
<div class="message_edit">
<div class="message_edit_form" id="{{id}}"></div>
<a class="message_edit_save">Save</a> <a class="message_edit_cancel">Cancel</a>
</div>
<div class="message_expander message_length_controller" title="See the rest of this message">[More...]</div>
<div class="message_condenser message_length_controller" title="Make this message take up less space on the screen">[Condense this message]</div>
</td>

View File

@@ -0,0 +1,8 @@
{{! Client-side Mustache template for rendering the trailing bookend.}}
<form>
{{#if is_stream}}
<span>Subject: <input type="text" value={{subject}} class="message_edit_subject"></input></span><br />
{{/if}}
<span>Content: <textarea class="message_edit_content">{{content}}</textarea></span>
</form>

View File

@@ -1038,6 +1038,20 @@ def json_tutorial_status(request, user_profile, status=REQ('status')):
def json_update_message(request, user_profile):
return update_message_backend(request, user_profile)
@authenticated_json_post_view
@has_request_variables
def json_fetch_raw_message(request, user_profile,
message_id=REQ(converter=to_non_negative_int)):
try:
message = Message.objects.get(id=message_id)
except Message.DoesNotExist:
return json_error("No such message")
if message.sender != user_profile:
return json_error("Message was not sent by you")
return json_success({"raw_content": message.content})
@has_request_variables
def update_message_backend(request, user_profile,
message_id=REQ(converter=to_non_negative_int),