From 2bdecd130a03ae2cb986a38c1c4679026b769fb0 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Tue, 14 May 2013 18:22:16 -0400 Subject: [PATCH] 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) --- humbug/settings.py | 1 + humbug/urls.py | 1 + tools/jslint/check-all.js | 2 +- zephyr/static/js/message_edit.js | 89 +++++++++++++++++++ zephyr/static/js/message_list.js | 19 ++++ zephyr/static/js/ui.js | 24 ++++- zephyr/static/styles/zephyr.css | 4 + .../actions_popover_content.handlebars | 8 ++ zephyr/static/templates/message.handlebars | 4 + .../templates/message_edit_form.handlebars | 8 ++ zephyr/views.py | 14 +++ 11 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 zephyr/static/js/message_edit.js create mode 100644 zephyr/static/templates/message_edit_form.handlebars diff --git a/humbug/settings.py b/humbug/settings.py index 5dfbf65f8d..d627184673 100644 --- a/humbug/settings.py +++ b/humbug/settings.py @@ -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', diff --git a/humbug/urls.py b/humbug/urls.py index fdc3033acb..57ce6d3716 100644 --- a/humbug/urls.py +++ b/humbug/urls.py @@ -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'), diff --git a/tools/jslint/check-all.js b/tools/jslint/check-all.js index 7c05392868..93dde23664 100644 --- a/tools/jslint/check-all.js +++ b/tools/jslint/check-all.js @@ -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' diff --git a/zephyr/static/js/message_edit.js b/zephyr/static/js/message_edit.js new file mode 100644 index 0000000000..76989dc163 --- /dev/null +++ b/zephyr/static/js/message_edit.js @@ -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; +}()); diff --git a/zephyr/static/js/message_list.js b/zephyr/static/js/message_list.js index 3cb1fb53d1..aa74247fc5 100644 --- a/zephyr/static/js/message_list.js +++ b/zephyr/static/js/message_list.js @@ -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 diff --git a/zephyr/static/js/ui.js b/zephyr/static/js/ui.js index c9e59c2154..8b9c008cfd 100644 --- a/zephyr/static/js/ui.js +++ b/zephyr/static/js/ui.js @@ -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) { diff --git a/zephyr/static/styles/zephyr.css b/zephyr/static/styles/zephyr.css index 8990033b42..eb4e1c9e2c 100644 --- a/zephyr/static/styles/zephyr.css +++ b/zephyr/static/styles/zephyr.css @@ -1435,3 +1435,7 @@ li.expanded_subject { text-align: center; padding-bottom: 10px; } + +.message_edit { + display: none; +} diff --git a/zephyr/static/templates/actions_popover_content.handlebars b/zephyr/static/templates/actions_popover_content.handlebars index c2b49142cd..3d8f4a7e2c 100644 --- a/zephyr/static/templates/actions_popover_content.handlebars +++ b/zephyr/static/templates/actions_popover_content.handlebars @@ -61,6 +61,14 @@ {{/if}} + {{#if can_edit_message}} +
  • + + Edit this message + +
  • + {{/if}} +
  • diff --git a/zephyr/static/templates/message.handlebars b/zephyr/static/templates/message.handlebars index 6ca57c1c2c..ba847e361b 100644 --- a/zephyr/static/templates/message.handlebars +++ b/zephyr/static/templates/message.handlebars @@ -78,6 +78,10 @@ {{/include_sender}}
    {{#if ../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}
    +
    [More...]
    [Condense this message]
    diff --git a/zephyr/static/templates/message_edit_form.handlebars b/zephyr/static/templates/message_edit_form.handlebars new file mode 100644 index 0000000000..f63fe50dad --- /dev/null +++ b/zephyr/static/templates/message_edit_form.handlebars @@ -0,0 +1,8 @@ +{{! Client-side Mustache template for rendering the trailing bookend.}} + +
    +{{#if is_stream}} + Subject:
    +{{/if}} + Content: +
    diff --git a/zephyr/views.py b/zephyr/views.py index 7babef7419..6c9be4e23c 100644 --- a/zephyr/views.py +++ b/zephyr/views.py @@ -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),