diff --git a/templates/zephyr/index.html b/templates/zephyr/index.html index f91a9d2fe0..c2e3c0bd77 100644 --- a/templates/zephyr/index.html +++ b/templates/zephyr/index.html @@ -55,6 +55,7 @@ + {% if debug %} diff --git a/tools/jslint/check-all.js b/tools/jslint/check-all.js index 08ffc36ae3..35972d6d79 100644 --- a/tools/jslint/check-all.js +++ b/tools/jslint/check-all.js @@ -45,6 +45,9 @@ var globals = // notifications.js + ' notifications' + // hashchange.js + + ' hashchange' + // ui.js + ' ui' diff --git a/zephyr/static-access-control/4nrjx8cwce2bka8r/js/hashchange.js b/zephyr/static-access-control/4nrjx8cwce2bka8r/js/hashchange.js new file mode 120000 index 0000000000..1f4df98915 --- /dev/null +++ b/zephyr/static-access-control/4nrjx8cwce2bka8r/js/hashchange.js @@ -0,0 +1 @@ +../../../static/js/hashchange.js \ No newline at end of file diff --git a/zephyr/static/js/hashchange.js b/zephyr/static/js/hashchange.js new file mode 100644 index 0000000000..d69f9e9dd1 --- /dev/null +++ b/zephyr/static/js/hashchange.js @@ -0,0 +1,38 @@ +var hashchange = (function () { + +var exports = {}; + +var expected_hash = false; + +exports.changehash = function (newhash) { + expected_hash = newhash; + window.location.hash = newhash; +}; + +function hashchanged() { + // If window.location.hash changed because our app explicitly + // changed it, then we don't need to do anything. + // (This function only neds to jump into action if it changed + // because e.g. the back button was pressed by the user) + if (window.location.hash === expected_hash) { + return; + } + + var hash = window.location.hash.split("/"); + if (hash[0] === "#narrow") { + narrow.hashchanged(hash); + ui.update_floating_recipient_bar(); + } + else if (narrow.active()) { + narrow.show_all_messages(); + ui.update_floating_recipient_bar(); + } +} + +exports.initialize = function () { + window.onhashchange = hashchanged; +}; + +return exports; + +}()); diff --git a/zephyr/static/js/narrow.js b/zephyr/static/js/narrow.js index eccff6f271..1ec758b8f4 100644 --- a/zephyr/static/js/narrow.js +++ b/zephyr/static/js/narrow.js @@ -44,6 +44,40 @@ exports.data = function () { return narrowdata; }; +exports.hashchanged = function (hash) { + var full_names, decoded, emails; + + if (hash[1] === "all_private_messages") { + exports.all_private_messages(); + } + else if (hash[1] === "stream" && hash[3] === "subject") { + exports.by_stream_and_subject_names(decodeURIComponent(hash[2]), + decodeURIComponent(hash[4])); + } + else if (hash[1] === "stream") { + exports.by_stream_name(decodeURIComponent(hash[2])); + } + else if (hash[1] === "private_messages") { + decoded = decodeURIComponent(hash[2]); + emails = decoded.split(", "); + + $.each(emails, function (index, email) { + $.each(people_list, function (index, person) { + if (person.email === email) { + if (full_names === undefined) { + full_names = person.full_name; + } + else { + full_names += ", " + person.full_name; + } + return false; + } + }); + }); + exports.by_private_message_group(full_names, decoded); + } +}; + function do_narrow(new_narrow, bar, time_travel, new_filter) { var was_narrowed = exports.active(); @@ -120,6 +154,8 @@ exports.target = function (id) { }; exports.all_private_messages = function () { + hashchange.changehash("#narrow/all_private_messages"); + var new_narrow = {type: "all_private_messages"}; var bar = {icon: 'user', description: 'You and anyone else'}; do_narrow(new_narrow, bar, false, function (other) { @@ -135,23 +171,33 @@ exports.by_subject = function () { exports.by_recipient(); return; } + exports.by_stream_and_subject_names(original.display_recipient, + original.subject); +}; - var new_narrow = {type: "subject", recipient_id: original.recipient_id, - subject: original.subject}; +exports.by_stream_and_subject_names = function (stream, subject) { + hashchange.changehash("#narrow/stream/" + + encodeURIComponent(stream) + + "/subject/" + + encodeURIComponent(subject)); + + var new_narrow = {type: "subject", stream: stream, subject: subject}; var bar = { - icon: 'bullhorn', - description: original.display_recipient, - subject: original.subject + icon: 'bullhorn', + description: stream, + subject: subject }; do_narrow(new_narrow, bar, false, function (other) { return ((other.type === 'stream') && - same_stream_and_subject(original, other)); + (other.display_recipient === stream && + other.subject.toLowerCase() === subject.toLowerCase())); }); }; -//TODO: There's probably some room to unify this code -// with the hotkey-narrowing code below. exports.by_stream_name = function (name) { + hashchange.changehash("#narrow/stream/" + + encodeURIComponent(name)); + var new_narrow = {type: "stream", stream: name}; var bar = {icon: 'bullhorn', description: name}; do_narrow(new_narrow, bar, false, function (other) { @@ -160,43 +206,38 @@ exports.by_stream_name = function (name) { }); }; -exports.by_private_message_partner = function (their_name, their_email) { - var new_narrow = {type: "private", one_on_one_email: their_email}; - var bar = {icon: 'user', description: "You and " + their_name}; +exports.by_private_message_group = function (names, emails) { + hashchange.changehash("#narrow/private_messages/" + + encodeURIComponent(emails)); + + var new_narrow = {type: "private", emails: emails.split(", ")}; + var bar = {icon: 'user', description: "You and " + names}; var my_email = email; do_narrow(new_narrow, bar, false, function (other) { return (other.type === 'private' && - get_private_message_recipient(other, 'email') === their_email); + other.reply_to === emails); }); }; // Called for the 'narrow by stream' hotkey. exports.by_recipient = function () { var message = message_dict[target_id]; - var bar; - var new_narrow; + var bar, new_narrow, emails; switch (message.type) { case 'private': - new_narrow = {type: "private", recipient_id: message.recipient_id}; - bar = {icon: 'user', description: "You and " + message.display_reply_to}; - do_narrow(new_narrow, bar, false, function (other) { - return (other.type === "private" && - other.reply_to === message.reply_to); - }); + exports.by_private_message_group(message.display_reply_to, + message.reply_to); break; case 'stream': - new_narrow = {type: "stream", recipient_id: message.recipient_id}; - bar = {icon: 'bullhorn', description: message.display_recipient}; - do_narrow(new_narrow, bar, false, function (other) { - return (other.type === 'stream' && - message.recipient_id === other.recipient_id); - }); + exports.by_stream_name(message.display_recipient); break; } }; exports.by_search_term = function (term) { + hashchange.changehash("#narrow/searchterm/" + encodeURIComponent(term)); + var new_narrow = {type: "searchterm", searchterm: term, allow_collapse: false}; var bar = {icon: 'search', description: 'Messages containing "' + term + '"'}; var term_lowercase = term.toLowerCase(); @@ -208,6 +249,8 @@ exports.by_search_term = function (term) { }; exports.show_all_messages = function () { + hashchange.changehash(""); + if (!narrowdata) { return; } diff --git a/zephyr/static/js/search.js b/zephyr/static/js/search.js index 16bea5cdd3..8bbdd33785 100644 --- a/zephyr/static/js/search.js +++ b/zephyr/static/js/search.js @@ -71,7 +71,7 @@ function narrow_or_search_for_term(item) { // unnarrow, it'll leave the searchbox. return ""; // Keep the search box empty } else if (obj.action === "private_message") { - narrow.by_private_message_partner(obj.query.full_name, obj.query.email); + narrow.by_private_message_group(obj.query.full_name, obj.query.email); return ""; } else if (obj.action === "search_narrow") { narrow.by_search_term(obj.query); diff --git a/zephyr/static/js/ui.js b/zephyr/static/js/ui.js index 1e1d0cbcdb..f955aff452 100644 --- a/zephyr/static/js/ui.js +++ b/zephyr/static/js/ui.js @@ -176,7 +176,7 @@ function hide_floating_recipient_bar() { } } -function update_floating_recipient_bar() { +exports.update_floating_recipient_bar = function () { var top_statusbar = $("#top_statusbar"); var top_statusbar_top = top_statusbar.offset().top; var top_statusbar_bottom = top_statusbar_top + top_statusbar.outerHeight(); @@ -239,7 +239,8 @@ function update_floating_recipient_bar() { } replace_floating_recipient_bar(current_label); -} +}; + function hack_for_floating_recipient_bar() { // So, as of this writing, Firefox respects visibility: collapse, // but WebKit does not (at least, my Chrome doesn't.) Instead it @@ -406,7 +407,7 @@ $(function () { $(window).scroll($.throttle(50, function (e) { if ($('#home').hasClass('active')) { keep_pointer_in_view(); - update_floating_recipient_bar(); + exports.update_floating_recipient_bar(); if (viewport.scrollTop() === 0 && have_scrolled_away_from_top) { have_scrolled_away_from_top = false; @@ -590,6 +591,7 @@ $(function () { composebox_typeahead.initialize(); search.initialize(); notifications.initialize(); + hashchange.initialize(); $("body").bind('click', function () { ui.hide_userinfo_popover(); diff --git a/zephyr/tests.py b/zephyr/tests.py index 298e4eda66..8b5cb34d03 100644 --- a/zephyr/tests.py +++ b/zephyr/tests.py @@ -643,12 +643,12 @@ class GetOldMessagesTest(AuthedTestCase): def test_bad_narrow_one_on_one_email_content(self): """ - If an invalid one_on_one_email is requested in get_old_messages, an + If an invalid 'emails' is requested in get_old_messages, an error is returned. """ self.login("hamlet@humbughq.com") - bad_stream_content = ("non-existent email", 0, []) - self.exercise_bad_narrow_content("one_on_one_email", bad_stream_content) + bad_stream_content = (["non-existent email"], "non-existent email", 0, []) + self.exercise_bad_narrow_content("emails", bad_stream_content) class DummyHandler(object): def __init__(self, assert_callback): diff --git a/zephyr/views.py b/zephyr/views.py index 3d1d5b81b4..85b9e3e0f4 100644 --- a/zephyr/views.py +++ b/zephyr/views.py @@ -283,20 +283,28 @@ def get_old_messages_backend(request, anchor = POST(converter=to_non_negative_in recipient = Recipient.objects.get(type=Recipient.STREAM, type_id=stream.id) query = query.filter(recipient_id = recipient.id) - if 'one_on_one_email' in narrow: + if 'emails' in narrow and (type(narrow['emails']) != type([]) or len(narrow['emails']) == 0): + return json_error("Invalid emails %s" % (narrow['emails'],)) + elif 'emails' in narrow and len(narrow['emails']) == 1: query = query.filter(recipient__type=Recipient.PERSONAL) try: - recipient_user = UserProfile.objects.get(user__email = narrow['one_on_one_email']) + recipient_user = UserProfile.objects.get(user__email = narrow['emails'][0]) except UserProfile.DoesNotExist: - return json_error("Invalid one_on_one_email %s" % (narrow['one_on_one_email'],)) + return json_error("Invalid emails ['" + narrow['emails'][0] + "']") recipient = Recipient.objects.get(type=Recipient.PERSONAL, type_id=recipient_user.id) # If we are narrowed to personals with ourself, we want to search for personals where the user - # with address "one_on_one_email" is the sender *and* the recipient, not personals where the user - # with address "one_on_one_email is the sender *or* the recipient. - if narrow['one_on_one_email'] == user_profile.user.email: - query = query.filter(Q(sender__user__email=narrow['one_on_one_email']) & Q(recipient=recipient)) + # with the desired e-mail address is the sender *and* the recipient, not personals where the + # user with the desired e-mail address is the sender *or* the recipient. + if narrow['emails'][0] == user_profile.user.email: + query = query.filter(Q(sender__user__email=narrow['emails'][0]) & Q(recipient=recipient)) else: - query = query.filter(Q(sender__user__email=narrow['one_on_one_email']) | Q(recipient=recipient)) + query = query.filter(Q(sender__user__email=narrow['emails'][0]) | Q(recipient=recipient)) + elif 'emails' in narrow: + try: + recipient = recipient_for_emails(narrow['emails'], False, user_profile, user_profile) + except ValidationError, e: + return json_error(e.messages[0]) + query = query.filter(recipient=recipient) elif 'type' in narrow and (narrow['type'] == "private" or narrow['type'] == "all_private_messages"): query = query.filter(Q(recipient__type=Recipient.PERSONAL) | Q(recipient__type=Recipient.HUDDLE)) @@ -611,6 +619,32 @@ def create_mirrored_message_users(request, user_profile, recipients): sender = UserProfile.objects.get(user__email=sender_email) return (True, sender) +def recipient_for_emails(emails, not_forged_zephyr_mirror, user_profile, sender): + recipient_profile_ids = set() + for email in emails: + try: + recipient_profile_ids.add(UserProfile.objects.get(user__email__iexact=email).id) + except UserProfile.DoesNotExist: + raise ValidationError("Invalid email '%s'" % (email,)) + + if not_forged_zephyr_mirror and user_profile.id not in recipient_profile_ids: + raise ValidationError("User not authorized for this query") + + # If the private message is just between the sender and + # another person, force it to be a personal internally + if (len(recipient_profile_ids) == 2 + and sender.id in recipient_profile_ids): + recipient_profile_ids.remove(sender.id) + + if len(recipient_profile_ids) > 1: + # Make sure the sender is included in huddle messages + recipient_profile_ids.add(sender.id) + huddle = get_huddle(list(recipient_profile_ids)) + return Recipient.objects.get(type_id=huddle.id, type=Recipient.HUDDLE) + else: + return Recipient.objects.get(type_id=list(recipient_profile_ids)[0], + type=Recipient.PERSONAL) + # We do not @require_login for send_message_backend, since it is used # both from the API and the web service. Code calling # send_message_backend should either check the API key or check that @@ -683,31 +717,11 @@ def send_message_backend(request, user_profile, client, return json_error("Stream does not exist") recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM) elif message_type_name == 'private': - recipient_profile_ids = set() - for email in message_to: - try: - recipient_profile_ids.add(UserProfile.objects.get(user__email__iexact=email).id) - except UserProfile.DoesNotExist: - return json_error("Invalid email '%s'" % (email,)) - - if client.name == "zephyr_mirror": - if user_profile.id not in recipient_profile_ids and not forged: - return json_error("User not authorized for this query") - - # If the private message is just between the sender and - # another person, force it to be a personal internally - if (len(recipient_profile_ids) == 2 - and sender.id in recipient_profile_ids): - recipient_profile_ids.remove(sender.id) - - if len(recipient_profile_ids) > 1: - # Make sure the sender is included in huddle messages - recipient_profile_ids.add(sender.id) - huddle = get_huddle(list(recipient_profile_ids)) - recipient = Recipient.objects.get(type_id=huddle.id, type=Recipient.HUDDLE) - else: - recipient = Recipient.objects.get(type_id=list(recipient_profile_ids)[0], - type=Recipient.PERSONAL) + not_forged_zephyr_mirror = client and client.name == "zephyr_mirror" and not forged + try: + recipient = recipient_for_emails(message_to, not_forged_zephyr_mirror, user_profile, sender) + except ValidationError, e: + return json_error(e.messages[0]) else: return json_error("Invalid message type")