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")