diff --git a/frontend_tests/node_tests/filter.js b/frontend_tests/node_tests/filter.js
index 140207b2fc..729a2ef84e 100644
--- a/frontend_tests/node_tests/filter.js
+++ b/frontend_tests/node_tests/filter.js
@@ -3,6 +3,9 @@ zrequire('unread');
zrequire('stream_data');
zrequire('people');
set_global('Handlebars', global.make_handlebars());
+global.stub_out_jquery();
+set_global('$', global.make_zjquery());
+zrequire('message_util', 'js/message_util');
zrequire('Filter', 'js/filter');
set_global('message_store', {});
@@ -121,7 +124,8 @@ run_test('basics', () => {
];
filter = new Filter(operators);
assert(filter.has_operator('has'));
- assert(!filter.can_apply_locally());
+ assert(filter.can_apply_locally());
+ assert(!filter.can_apply_locally(true));
assert(!filter.includes_full_stream_history());
assert(!filter.can_mark_messages_read());
assert(!filter.is_personal_filter());
@@ -715,6 +719,62 @@ run_test('predicate_basics', () => {
display_recipient: [{id: steve.user_id}, {id: me.user_id}],
}));
assert(!predicate({type: 'stream'}));
+
+ const img_msg = {
+ content: `
test.jpeg
`,
+ };
+
+ const link_msg = {
+ content: `chat.zulip.org
`,
+ };
+
+ const non_img_attachment_msg = {
+ content: `attachment.ext
`,
+ };
+
+ const no_has_filter_matching_msg = {
+ content: "Testing
",
+ };
+
+ predicate = get_predicate([['has', 'non_valid_operand']]);
+ assert(!predicate(img_msg));
+ assert(!predicate(non_img_attachment_msg));
+ assert(!predicate(link_msg));
+ assert(!predicate(no_has_filter_matching_msg));
+
+ // HTML content of message is used to determine if image have link, image or attachment.
+ // We are using jquery to parse the html and find existence of relevant tags/elements.
+ // In tests we need to stub the calls to jquery so using zjquery's .set_find_results method.
+ const has_link = get_predicate([['has', 'link']]);
+ $(img_msg.content).set_find_results("a", [$("")]);
+ assert(has_link(img_msg));
+ $(non_img_attachment_msg.content).set_find_results("a", [$("")]);
+ assert(has_link(non_img_attachment_msg));
+ $(link_msg.content).set_find_results("a", [$("")]);
+ assert(has_link(link_msg));
+ $(no_has_filter_matching_msg.content).set_find_results("a", false);
+ assert(!has_link(no_has_filter_matching_msg));
+
+ const has_attachment = get_predicate([['has', 'attachment']]);
+ $(img_msg.content).set_find_results("a[href^='/user_uploads']", [$("")]);
+ assert(has_attachment(img_msg));
+ $(non_img_attachment_msg.content).set_find_results("a[href^='/user_uploads']", [$("")]);
+ assert(has_attachment(non_img_attachment_msg));
+ $(link_msg.content).set_find_results("a[href^='/user_uploads']", false);
+ assert(!has_attachment(link_msg));
+ $(no_has_filter_matching_msg.content).set_find_results("a[href^='/user_uploads']", false);
+ assert(!has_attachment(no_has_filter_matching_msg));
+
+ const has_image = get_predicate([['has', 'image']]);
+ $(img_msg.content).set_find_results(".message_inline_image", [$("
")]);
+ assert(has_image(img_msg));
+ $(non_img_attachment_msg.content).set_find_results(".message_inline_image", false);
+ assert(!has_image(non_img_attachment_msg));
+ $(link_msg.content).set_find_results(".message_inline_image", false);
+ assert(!has_image(link_msg));
+ $(no_has_filter_matching_msg.content).set_find_results(".message_inline_image", false);
+ assert(!has_image(no_has_filter_matching_msg));
+
});
run_test('negated_predicates', () => {
diff --git a/static/js/echo.js b/static/js/echo.js
index b3714abdeb..aa48e6f801 100644
--- a/static/js/echo.js
+++ b/static/js/echo.js
@@ -146,7 +146,7 @@ exports.try_deliver_locally = function (message_request) {
return;
}
- if (narrow_state.active() && !narrow_state.filter().can_apply_locally()) {
+ if (narrow_state.active() && !narrow_state.filter().can_apply_locally(true)) {
return;
}
diff --git a/static/js/filter.js b/static/js/filter.js
index 231e1ef0f3..436a742f27 100644
--- a/static/js/filter.js
+++ b/static/js/filter.js
@@ -46,6 +46,15 @@ function message_in_home(message) {
function message_matches_search_term(message, operator, operand) {
switch (operator) {
+ case 'has':
+ if (operand === 'image') {
+ return message_util.message_has_image(message);
+ } else if (operand === 'link') {
+ return message_util.message_has_link(message);
+ } else if (operand === 'attachment') {
+ return message_util.message_has_attachment(message);
+ }
+ return false; // has:something_else returns false
case 'is':
if (operand === 'private') {
return message.type === 'private';
@@ -620,7 +629,10 @@ Filter.prototype = {
return this.has_operand("is", "mentioned") || this.has_operand("is", "starred");
},
- can_apply_locally: function () {
+ can_apply_locally: function (is_local_echo) {
+ // Since there can be multiple operators, each block should
+ // just return false here.
+
if (this.is_search()) {
// The semantics for matching keywords are implemented
// by database plugins, and we don't have JS code for
@@ -629,10 +641,11 @@ Filter.prototype = {
return false;
}
- if (this.has_operator('has')) {
- // See #6186 to see why we currently punt on 'has:foo'
- // queries. This can be fixed, there are just some random
- // complications that make it non-trivial.
+ if (this.has_operator('has') && is_local_echo) {
+ // The has: operators can be applied locally for messages
+ // rendered by the backend; links, attachments, and images
+ // are not handled properly by the local echo markdown
+ // processor.
return false;
}
diff --git a/static/js/message_util.js b/static/js/message_util.js
index 8467542bef..dca374b1e3 100644
--- a/static/js/message_util.js
+++ b/static/js/message_util.js
@@ -16,6 +16,18 @@ function add_messages(messages, msg_list, opts) {
return render_info;
}
+exports.message_has_link = function (message) {
+ return $(message.content).find(`a`).length > 0;
+};
+
+exports.message_has_image = function (message) {
+ return $(message.content).find(`.message_inline_image`).length > 0;
+};
+
+exports.message_has_attachment = function (message) {
+ return $(message.content).find(`a[href^='/user_uploads']`).length > 0;
+};
+
exports.add_old_messages = function (messages, msg_list) {
return add_messages(messages, msg_list, {messages_are_new: false});
};