Make the back button work when narrowed

(imported from commit be922b06e7b53ec21751e67a490bb518333c3e6c)
This commit is contained in:
Jeff Arnold
2012-12-07 14:52:39 -05:00
parent a2f26f1106
commit db6f03d46d
9 changed files with 168 additions and 66 deletions

View File

@@ -55,6 +55,7 @@
<script type="text/javascript" src="{{ static_hidden }}js/composebox_typeahead.js"></script> <script type="text/javascript" src="{{ static_hidden }}js/composebox_typeahead.js"></script>
<script type="text/javascript" src="{{ static_hidden }}js/hotkey.js"></script> <script type="text/javascript" src="{{ static_hidden }}js/hotkey.js"></script>
<script type="text/javascript" src="{{ static_hidden }}js/notifications.js"></script> <script type="text/javascript" src="{{ static_hidden }}js/notifications.js"></script>
<script type="text/javascript" src="{{ static_hidden }}js/hashchange.js"></script>
<script type="text/javascript" src="{{ static_hidden }}js/zephyr.js"></script> <script type="text/javascript" src="{{ static_hidden }}js/zephyr.js"></script>
{% if debug %} {% if debug %}

View File

@@ -45,6 +45,9 @@ var globals =
// notifications.js // notifications.js
+ ' notifications' + ' notifications'
// hashchange.js
+ ' hashchange'
// ui.js // ui.js
+ ' ui' + ' ui'

View File

@@ -0,0 +1 @@
../../../static/js/hashchange.js

View File

@@ -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;
}());

View File

@@ -44,6 +44,40 @@ exports.data = function () {
return narrowdata; 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) { function do_narrow(new_narrow, bar, time_travel, new_filter) {
var was_narrowed = exports.active(); var was_narrowed = exports.active();
@@ -120,6 +154,8 @@ exports.target = function (id) {
}; };
exports.all_private_messages = function () { exports.all_private_messages = function () {
hashchange.changehash("#narrow/all_private_messages");
var new_narrow = {type: "all_private_messages"}; var new_narrow = {type: "all_private_messages"};
var bar = {icon: 'user', description: 'You and anyone else'}; var bar = {icon: 'user', description: 'You and anyone else'};
do_narrow(new_narrow, bar, false, function (other) { do_narrow(new_narrow, bar, false, function (other) {
@@ -135,23 +171,33 @@ exports.by_subject = function () {
exports.by_recipient(); exports.by_recipient();
return; return;
} }
exports.by_stream_and_subject_names(original.display_recipient,
original.subject);
};
var new_narrow = {type: "subject", recipient_id: original.recipient_id, exports.by_stream_and_subject_names = function (stream, subject) {
subject: original.subject}; hashchange.changehash("#narrow/stream/" +
encodeURIComponent(stream) +
"/subject/" +
encodeURIComponent(subject));
var new_narrow = {type: "subject", stream: stream, subject: subject};
var bar = { var bar = {
icon: 'bullhorn', icon: 'bullhorn',
description: original.display_recipient, description: stream,
subject: original.subject subject: subject
}; };
do_narrow(new_narrow, bar, false, function (other) { do_narrow(new_narrow, bar, false, function (other) {
return ((other.type === 'stream') && 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) { exports.by_stream_name = function (name) {
hashchange.changehash("#narrow/stream/" +
encodeURIComponent(name));
var new_narrow = {type: "stream", stream: name}; var new_narrow = {type: "stream", stream: name};
var bar = {icon: 'bullhorn', description: name}; var bar = {icon: 'bullhorn', description: name};
do_narrow(new_narrow, bar, false, function (other) { 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) { exports.by_private_message_group = function (names, emails) {
var new_narrow = {type: "private", one_on_one_email: their_email}; hashchange.changehash("#narrow/private_messages/" +
var bar = {icon: 'user', description: "You and " + their_name}; encodeURIComponent(emails));
var new_narrow = {type: "private", emails: emails.split(", ")};
var bar = {icon: 'user', description: "You and " + names};
var my_email = email; var my_email = email;
do_narrow(new_narrow, bar, false, function (other) { do_narrow(new_narrow, bar, false, function (other) {
return (other.type === 'private' && return (other.type === 'private' &&
get_private_message_recipient(other, 'email') === their_email); other.reply_to === emails);
}); });
}; };
// Called for the 'narrow by stream' hotkey. // Called for the 'narrow by stream' hotkey.
exports.by_recipient = function () { exports.by_recipient = function () {
var message = message_dict[target_id]; var message = message_dict[target_id];
var bar; var bar, new_narrow, emails;
var new_narrow;
switch (message.type) { switch (message.type) {
case 'private': case 'private':
new_narrow = {type: "private", recipient_id: message.recipient_id}; exports.by_private_message_group(message.display_reply_to,
bar = {icon: 'user', description: "You and " + message.display_reply_to}; message.reply_to);
do_narrow(new_narrow, bar, false, function (other) {
return (other.type === "private" &&
other.reply_to === message.reply_to);
});
break; break;
case 'stream': case 'stream':
new_narrow = {type: "stream", recipient_id: message.recipient_id}; exports.by_stream_name(message.display_recipient);
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);
});
break; break;
} }
}; };
exports.by_search_term = function (term) { exports.by_search_term = function (term) {
hashchange.changehash("#narrow/searchterm/" + encodeURIComponent(term));
var new_narrow = {type: "searchterm", searchterm: term, allow_collapse: false}; var new_narrow = {type: "searchterm", searchterm: term, allow_collapse: false};
var bar = {icon: 'search', description: 'Messages containing "' + term + '"'}; var bar = {icon: 'search', description: 'Messages containing "' + term + '"'};
var term_lowercase = term.toLowerCase(); var term_lowercase = term.toLowerCase();
@@ -208,6 +249,8 @@ exports.by_search_term = function (term) {
}; };
exports.show_all_messages = function () { exports.show_all_messages = function () {
hashchange.changehash("");
if (!narrowdata) { if (!narrowdata) {
return; return;
} }

View File

@@ -71,7 +71,7 @@ function narrow_or_search_for_term(item) {
// unnarrow, it'll leave the searchbox. // unnarrow, it'll leave the searchbox.
return ""; // Keep the search box empty return ""; // Keep the search box empty
} else if (obj.action === "private_message") { } 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 ""; return "";
} else if (obj.action === "search_narrow") { } else if (obj.action === "search_narrow") {
narrow.by_search_term(obj.query); narrow.by_search_term(obj.query);

View File

@@ -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_statusbar");
var top_statusbar_top = top_statusbar.offset().top; var top_statusbar_top = top_statusbar.offset().top;
var top_statusbar_bottom = top_statusbar_top + top_statusbar.outerHeight(); 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); replace_floating_recipient_bar(current_label);
} };
function hack_for_floating_recipient_bar() { function hack_for_floating_recipient_bar() {
// So, as of this writing, Firefox respects visibility: collapse, // So, as of this writing, Firefox respects visibility: collapse,
// but WebKit does not (at least, my Chrome doesn't.) Instead it // but WebKit does not (at least, my Chrome doesn't.) Instead it
@@ -406,7 +407,7 @@ $(function () {
$(window).scroll($.throttle(50, function (e) { $(window).scroll($.throttle(50, function (e) {
if ($('#home').hasClass('active')) { if ($('#home').hasClass('active')) {
keep_pointer_in_view(); keep_pointer_in_view();
update_floating_recipient_bar(); exports.update_floating_recipient_bar();
if (viewport.scrollTop() === 0 && if (viewport.scrollTop() === 0 &&
have_scrolled_away_from_top) { have_scrolled_away_from_top) {
have_scrolled_away_from_top = false; have_scrolled_away_from_top = false;
@@ -590,6 +591,7 @@ $(function () {
composebox_typeahead.initialize(); composebox_typeahead.initialize();
search.initialize(); search.initialize();
notifications.initialize(); notifications.initialize();
hashchange.initialize();
$("body").bind('click', function () { $("body").bind('click', function () {
ui.hide_userinfo_popover(); ui.hide_userinfo_popover();

View File

@@ -643,12 +643,12 @@ class GetOldMessagesTest(AuthedTestCase):
def test_bad_narrow_one_on_one_email_content(self): 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. error is returned.
""" """
self.login("hamlet@humbughq.com") self.login("hamlet@humbughq.com")
bad_stream_content = ("non-existent email", 0, []) bad_stream_content = (["non-existent email"], "non-existent email", 0, [])
self.exercise_bad_narrow_content("one_on_one_email", bad_stream_content) self.exercise_bad_narrow_content("emails", bad_stream_content)
class DummyHandler(object): class DummyHandler(object):
def __init__(self, assert_callback): def __init__(self, assert_callback):

View File

@@ -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) recipient = Recipient.objects.get(type=Recipient.STREAM, type_id=stream.id)
query = query.filter(recipient_id = recipient.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) query = query.filter(recipient__type=Recipient.PERSONAL)
try: 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: 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) 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 # 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 the desired e-mail address is the sender *and* the recipient, not personals where the
# with address "one_on_one_email is the sender *or* the recipient. # user with the desired e-mail address is the sender *or* the recipient.
if narrow['one_on_one_email'] == user_profile.user.email: if narrow['emails'][0] == user_profile.user.email:
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))
else: 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"): 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)) 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) sender = UserProfile.objects.get(user__email=sender_email)
return (True, sender) 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 # We do not @require_login for send_message_backend, since it is used
# both from the API and the web service. Code calling # both from the API and the web service. Code calling
# send_message_backend should either check the API key or check that # 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") return json_error("Stream does not exist")
recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM) recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM)
elif message_type_name == 'private': elif message_type_name == 'private':
recipient_profile_ids = set() not_forged_zephyr_mirror = client and client.name == "zephyr_mirror" and not forged
for email in message_to:
try: try:
recipient_profile_ids.add(UserProfile.objects.get(user__email__iexact=email).id) recipient = recipient_for_emails(message_to, not_forged_zephyr_mirror, user_profile, sender)
except UserProfile.DoesNotExist: except ValidationError, e:
return json_error("Invalid email '%s'" % (email,)) return json_error(e.messages[0])
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)
else: else:
return json_error("Invalid message type") return json_error("Invalid message type")