mirror of https://github.com/zulip/zulip.git
search: Add is:followed filter.
Create the is:followed search operator. Fetch all messages that are from followed topics using exists. Update API documentation and changelog. Co-authored-by: Kenneth Rodrigues <kenneth.nrk123@gmail.com> Fixes #27309.
This commit is contained in:
parent
4ac527d327
commit
81ea09be19
|
@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 9.0
|
## Changes in Zulip 9.0
|
||||||
|
|
||||||
|
**Feature level 265**
|
||||||
|
|
||||||
|
* [`GET /messages`](/api/get-messages),
|
||||||
|
[`GET /messages/matches_narrow`](/api/check-messages-match-narrow),
|
||||||
|
[`POST /messages/flags/narrow`](/api/update-message-flags-for-narrow),
|
||||||
|
[`POST /register`](/api/register-queue):
|
||||||
|
Added a new [search/narrow filter](/api/construct-narrow),
|
||||||
|
`is:followed`, matching messages in topics that the current user is
|
||||||
|
[following](/help/follow-a-topic).
|
||||||
|
|
||||||
**Feature level 264**
|
**Feature level 264**
|
||||||
|
|
||||||
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
||||||
|
|
|
@ -51,8 +51,12 @@ important optimization when fetching messages in certain cases (e.g.
|
||||||
when [adding the `read` flag to a user's personal
|
when [adding the `read` flag to a user's personal
|
||||||
messages](/api/update-message-flags-for-narrow)).
|
messages](/api/update-message-flags-for-narrow)).
|
||||||
|
|
||||||
**Changes**: In Zulip 9.0 (feature level 250), support was added for
|
**Changes**: In Zulip 9.0 (feature level 265), support was added for a
|
||||||
two filters related to channel messages: `channel` and `channels`. The
|
new `is:followed` filter, matching messages in topics that the current
|
||||||
|
user is [following](/help/follow-a-topic).
|
||||||
|
|
||||||
|
In Zulip 9.0 (feature level 250), support was added for
|
||||||
|
two filters related to stream messages: `channel` and `channels`. The
|
||||||
`channel` operator is an alias for the `stream` operator. The `channels`
|
`channel` operator is an alias for the `stream` operator. The `channels`
|
||||||
operator is an alias for the `streams` operator. Both `channel` and
|
operator is an alias for the `streams` operator. Both `channel` and
|
||||||
`channels` return the same exact results as `stream` and `streams`
|
`channels` return the same exact results as `stream` and `streams`
|
||||||
|
|
|
@ -116,6 +116,7 @@ Zulip offers the following filters based on the location of the message.
|
||||||
|
|
||||||
### Search by message status
|
### Search by message status
|
||||||
|
|
||||||
|
* `is:followed`: Search messages in [followed topics](/help/follow-a-topic).
|
||||||
* `is:resolved`: Search messages in [resolved topics](/help/resolve-a-topic).
|
* `is:resolved`: Search messages in [resolved topics](/help/resolve-a-topic).
|
||||||
* `-is:resolved`: Search messages in [unresolved topics](/help/resolve-a-topic).
|
* `-is:resolved`: Search messages in [unresolved topics](/help/resolve-a-topic).
|
||||||
* `is:unread`: Search your unread messages.
|
* `is:unread`: Search your unread messages.
|
||||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
API_FEATURE_LEVEL = 264
|
API_FEATURE_LEVEL = 265
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||||
# only when going from an old version of the code to a newer version. Bump
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -165,6 +165,11 @@ function message_matches_search_term(message: Message, operator: string, operand
|
||||||
return unread.message_unread(message);
|
return unread.message_unread(message);
|
||||||
case "resolved":
|
case "resolved":
|
||||||
return message.type === "stream" && resolved_topic.is_resolved(message.topic);
|
return message.type === "stream" && resolved_topic.is_resolved(message.topic);
|
||||||
|
case "followed":
|
||||||
|
return (
|
||||||
|
message.type === "stream" &&
|
||||||
|
user_topics.is_topic_followed(message.stream_id, message.topic)
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return false; // is:whatever returns false
|
return false; // is:whatever returns false
|
||||||
}
|
}
|
||||||
|
@ -503,6 +508,7 @@ export class Filter {
|
||||||
"is-starred",
|
"is-starred",
|
||||||
"is-unread",
|
"is-unread",
|
||||||
"is-resolved",
|
"is-resolved",
|
||||||
|
"is-followed",
|
||||||
"has-link",
|
"has-link",
|
||||||
"has-image",
|
"has-image",
|
||||||
"has-attachment",
|
"has-attachment",
|
||||||
|
@ -753,6 +759,8 @@ export class Filter {
|
||||||
"not-is-dm",
|
"not-is-dm",
|
||||||
"is-resolved",
|
"is-resolved",
|
||||||
"not-is-resolved",
|
"not-is-resolved",
|
||||||
|
"is-followed",
|
||||||
|
"not-is-followed",
|
||||||
"in-home",
|
"in-home",
|
||||||
"in-all",
|
"in-all",
|
||||||
"channels-public",
|
"channels-public",
|
||||||
|
@ -853,6 +861,9 @@ export class Filter {
|
||||||
if (_.isEqual(term_types, ["sender"])) {
|
if (_.isEqual(term_types, ["sender"])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (_.isEqual(term_types, ["is-followed"])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
_.isEqual(term_types, ["sender", "has-reaction"]) &&
|
_.isEqual(term_types, ["sender", "has-reaction"]) &&
|
||||||
this.operands("sender")[0] === people.my_current_email()
|
this.operands("sender")[0] === people.my_current_email()
|
||||||
|
@ -924,6 +935,8 @@ export class Filter {
|
||||||
return "/#narrow/dm/" + people.emails_to_slug(this.operands("dm").join(","));
|
return "/#narrow/dm/" + people.emails_to_slug(this.operands("dm").join(","));
|
||||||
case "is-resolved":
|
case "is-resolved":
|
||||||
return "/#narrow/topics/is/resolved";
|
return "/#narrow/topics/is/resolved";
|
||||||
|
case "is-followed":
|
||||||
|
return "/#narrow/topics/is/followed";
|
||||||
// TODO: It is ambiguous how we want to handle the 'sender' case,
|
// TODO: It is ambiguous how we want to handle the 'sender' case,
|
||||||
// we may remove it in the future based on design decisions
|
// we may remove it in the future based on design decisions
|
||||||
case "sender":
|
case "sender":
|
||||||
|
@ -988,6 +1001,9 @@ export class Filter {
|
||||||
case "is-resolved":
|
case "is-resolved":
|
||||||
icon = "check";
|
icon = "check";
|
||||||
break;
|
break;
|
||||||
|
case "is-followed":
|
||||||
|
zulip_icon = "follow";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
icon = undefined;
|
icon = undefined;
|
||||||
break;
|
break;
|
||||||
|
@ -1069,6 +1085,8 @@ export class Filter {
|
||||||
return $t({defaultMessage: "Direct message feed"});
|
return $t({defaultMessage: "Direct message feed"});
|
||||||
case "is-resolved":
|
case "is-resolved":
|
||||||
return $t({defaultMessage: "Topics marked as resolved"});
|
return $t({defaultMessage: "Topics marked as resolved"});
|
||||||
|
case "is-followed":
|
||||||
|
return $t({defaultMessage: "Followed topics"});
|
||||||
// These cases return false for is_common_narrow, and therefore are not
|
// These cases return false for is_common_narrow, and therefore are not
|
||||||
// formatted in the message view header. They are used in narrow.js to
|
// formatted in the message view header. They are used in narrow.js to
|
||||||
// update the browser title.
|
// update the browser title.
|
||||||
|
@ -1103,6 +1121,13 @@ export class Filter {
|
||||||
}),
|
}),
|
||||||
link: "/help/star-a-message#view-your-starred-messages",
|
link: "/help/star-a-message#view-your-starred-messages",
|
||||||
};
|
};
|
||||||
|
case "is-followed":
|
||||||
|
return {
|
||||||
|
description: $t({
|
||||||
|
defaultMessage: "Messages in topics you follow.",
|
||||||
|
}),
|
||||||
|
link: "/help/follow-a-topic",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
_.isEqual(term_types, ["sender", "has-reaction"]) &&
|
_.isEqual(term_types, ["sender", "has-reaction"]) &&
|
||||||
|
|
|
@ -261,6 +261,10 @@ function pick_empty_narrow_banner(): NarrowBannerData {
|
||||||
return {
|
return {
|
||||||
title: $t({defaultMessage: "No topics are marked as resolved."}),
|
title: $t({defaultMessage: "No topics are marked as resolved."}),
|
||||||
};
|
};
|
||||||
|
case "followed":
|
||||||
|
return {
|
||||||
|
title: $t({defaultMessage: "You aren't following any topics."}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// fallthrough to default case if no match is found
|
// fallthrough to default case if no match is found
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -599,6 +599,16 @@ function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugge
|
||||||
description_html: "@-mentions",
|
description_html: "@-mentions",
|
||||||
incompatible_patterns: [{operator: "is", operand: "mentioned"}],
|
incompatible_patterns: [{operator: "is", operand: "mentioned"}],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
search_string: "is:followed",
|
||||||
|
description_html: "followed topics",
|
||||||
|
incompatible_patterns: [
|
||||||
|
{operator: "is", operand: "followed"},
|
||||||
|
{operator: "is", operand: "dm"},
|
||||||
|
{operator: "dm"},
|
||||||
|
{operator: "dm-including"},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
search_string: "is:alerted",
|
search_string: "is:alerted",
|
||||||
description_html: "alerted messages",
|
description_html: "alerted messages",
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
{{~!-- squash whitespace --~}}
|
{{~!-- squash whitespace --~}}
|
||||||
{{this.verb}}topics marked as resolved
|
{{this.verb}}topics marked as resolved
|
||||||
{{~!-- squash whitespace --~}}
|
{{~!-- squash whitespace --~}}
|
||||||
|
{{else if (eq this.operand "followed")}}
|
||||||
|
{{~!-- squash whitespace --~}}
|
||||||
|
{{this.verb}}followed topics
|
||||||
|
{{~!-- squash whitespace --~}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{~!-- squash whitespace --~}}
|
{{~!-- squash whitespace --~}}
|
||||||
invalid {{this.operand}} operand for is operator
|
invalid {{this.operand}} operand for is operator
|
||||||
|
|
|
@ -129,6 +129,12 @@
|
||||||
{{t 'Narrow to messages in resolved topics.'}}
|
{{t 'Narrow to messages in resolved topics.'}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="operator">is:followed</td>
|
||||||
|
<td class="definition">
|
||||||
|
{{t 'Narrow to messages in followed topics.'}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="operator">is:unread</td>
|
<td class="operator">is:unread</td>
|
||||||
<td class="definition">
|
<td class="definition">
|
||||||
|
|
|
@ -361,6 +361,7 @@ test("basics", () => {
|
||||||
// filter.supports_collapsing_recipients loop.
|
// filter.supports_collapsing_recipients loop.
|
||||||
terms = [
|
terms = [
|
||||||
{operator: "is", operand: "resolved", negated: true},
|
{operator: "is", operand: "resolved", negated: true},
|
||||||
|
{operator: "is", operand: "followed", negated: true},
|
||||||
{operator: "is", operand: "dm", negated: true},
|
{operator: "is", operand: "dm", negated: true},
|
||||||
{operator: "channel", operand: "channel_name", negated: true},
|
{operator: "channel", operand: "channel_name", negated: true},
|
||||||
{operator: "channels", operand: "web-public", negated: true},
|
{operator: "channels", operand: "web-public", negated: true},
|
||||||
|
@ -490,6 +491,10 @@ function assert_not_mark_read_with_is_operands(additional_terms_to_test) {
|
||||||
is_operator = [{operator: "is", operand: "resolved", negated: true}];
|
is_operator = [{operator: "is", operand: "resolved", negated: true}];
|
||||||
filter = new Filter([...additional_terms_to_test, ...is_operator]);
|
filter = new Filter([...additional_terms_to_test, ...is_operator]);
|
||||||
assert.ok(!filter.can_mark_messages_read());
|
assert.ok(!filter.can_mark_messages_read());
|
||||||
|
|
||||||
|
is_operator = [{operator: "is", operand: "followed", negated: true}];
|
||||||
|
filter = new Filter([...additional_terms_to_test, ...is_operator]);
|
||||||
|
assert.ok(!filter.can_mark_messages_read());
|
||||||
}
|
}
|
||||||
|
|
||||||
function assert_not_mark_read_when_searching(additional_terms_to_test) {
|
function assert_not_mark_read_when_searching(additional_terms_to_test) {
|
||||||
|
@ -859,6 +864,14 @@ test("predicate_basics", ({override}) => {
|
||||||
assert.ok(!predicate({topic: resolved_topic_name}));
|
assert.ok(!predicate({topic: resolved_topic_name}));
|
||||||
assert.ok(!predicate({type: stream_message, topic: "foo"}));
|
assert.ok(!predicate({type: stream_message, topic: "foo"}));
|
||||||
|
|
||||||
|
predicate = get_predicate([["is", "followed"]]);
|
||||||
|
|
||||||
|
override(user_topics, "is_topic_followed", () => false);
|
||||||
|
assert.ok(!predicate({type: "stream", topic: "foo", stream_id: 5}));
|
||||||
|
|
||||||
|
override(user_topics, "is_topic_followed", () => true);
|
||||||
|
assert.ok(predicate({type: "stream", topic: "foo", stream_id: 5}));
|
||||||
|
|
||||||
const unknown_stream_id = 999;
|
const unknown_stream_id = 999;
|
||||||
override(user_topics, "is_topic_muted", () => false);
|
override(user_topics, "is_topic_muted", () => false);
|
||||||
override(user_topics, "is_topic_unmuted_or_followed", () => false);
|
override(user_topics, "is_topic_unmuted_or_followed", () => false);
|
||||||
|
@ -1406,6 +1419,10 @@ test("describe", ({mock_template}) => {
|
||||||
string = "topics marked as resolved";
|
string = "topics marked as resolved";
|
||||||
assert.equal(Filter.search_description_as_html(narrow), string);
|
assert.equal(Filter.search_description_as_html(narrow), string);
|
||||||
|
|
||||||
|
narrow = [{operator: "is", operand: "followed"}];
|
||||||
|
string = "followed topics";
|
||||||
|
assert.equal(Filter.search_description_as_html(narrow), string);
|
||||||
|
|
||||||
narrow = [{operator: "is", operand: "something_we_do_not_support"}];
|
narrow = [{operator: "is", operand: "something_we_do_not_support"}];
|
||||||
string = "invalid something_we_do_not_support operand for is operator";
|
string = "invalid something_we_do_not_support operand for is operator";
|
||||||
assert.equal(Filter.search_description_as_html(narrow), string);
|
assert.equal(Filter.search_description_as_html(narrow), string);
|
||||||
|
@ -1739,6 +1756,7 @@ test("navbar_helpers", () => {
|
||||||
const is_dm = [{operator: "is", operand: "dm"}];
|
const is_dm = [{operator: "is", operand: "dm"}];
|
||||||
const is_mentioned = [{operator: "is", operand: "mentioned"}];
|
const is_mentioned = [{operator: "is", operand: "mentioned"}];
|
||||||
const is_resolved = [{operator: "is", operand: "resolved"}];
|
const is_resolved = [{operator: "is", operand: "resolved"}];
|
||||||
|
const is_followed = [{operator: "is", operand: "followed"}];
|
||||||
const channels_public = [{operator: "channels", operand: "public"}];
|
const channels_public = [{operator: "channels", operand: "public"}];
|
||||||
const channel_topic_terms = [
|
const channel_topic_terms = [
|
||||||
{operator: "channel", operand: "foo"},
|
{operator: "channel", operand: "foo"},
|
||||||
|
@ -1842,6 +1860,15 @@ test("navbar_helpers", () => {
|
||||||
title: "translated: Topics marked as resolved",
|
title: "translated: Topics marked as resolved",
|
||||||
redirect_url_with_search: "/#narrow/topics/is/resolved",
|
redirect_url_with_search: "/#narrow/topics/is/resolved",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
terms: is_followed,
|
||||||
|
is_common_narrow: true,
|
||||||
|
zulip_icon: "follow",
|
||||||
|
title: "translated: Followed topics",
|
||||||
|
redirect_url_with_search: "/#narrow/topics/is/followed",
|
||||||
|
description: "translated: Messages in topics you follow.",
|
||||||
|
link: "/help/follow-a-topic",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
terms: channel_topic_terms,
|
terms: channel_topic_terms,
|
||||||
is_common_narrow: true,
|
is_common_narrow: true,
|
||||||
|
|
|
@ -337,6 +337,13 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
|
||||||
empty_narrow_html("translated: No topics are marked as resolved."),
|
empty_narrow_html("translated: No topics are marked as resolved."),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
set_filter([["is", "followed"]]);
|
||||||
|
narrow_banner.show_empty_narrow_message();
|
||||||
|
assert.equal(
|
||||||
|
$(".empty_feed_notice_main").html(),
|
||||||
|
empty_narrow_html("translated: You aren't following any topics."),
|
||||||
|
);
|
||||||
|
|
||||||
// organization has disabled sending direct messages
|
// organization has disabled sending direct messages
|
||||||
realm.realm_private_message_policy =
|
realm.realm_private_message_policy =
|
||||||
settings_config.private_message_policy_values.disabled.code;
|
settings_config.private_message_policy_values.disabled.code;
|
||||||
|
|
|
@ -438,6 +438,7 @@ test("empty_query_suggestions", () => {
|
||||||
"is:dm",
|
"is:dm",
|
||||||
"is:starred",
|
"is:starred",
|
||||||
"is:mentioned",
|
"is:mentioned",
|
||||||
|
"is:followed",
|
||||||
"is:alerted",
|
"is:alerted",
|
||||||
"is:unread",
|
"is:unread",
|
||||||
"is:resolved",
|
"is:resolved",
|
||||||
|
@ -461,6 +462,7 @@ test("empty_query_suggestions", () => {
|
||||||
assert.equal(describe("is:alerted"), "Alerted messages");
|
assert.equal(describe("is:alerted"), "Alerted messages");
|
||||||
assert.equal(describe("is:unread"), "Unread messages");
|
assert.equal(describe("is:unread"), "Unread messages");
|
||||||
assert.equal(describe("is:resolved"), "Topics marked as resolved");
|
assert.equal(describe("is:resolved"), "Topics marked as resolved");
|
||||||
|
assert.equal(describe("is:followed"), "Followed topics");
|
||||||
assert.equal(describe("sender:myself@zulip.com"), "Sent by me");
|
assert.equal(describe("sender:myself@zulip.com"), "Sent by me");
|
||||||
assert.equal(describe("has:link"), "Messages that contain links");
|
assert.equal(describe("has:link"), "Messages that contain links");
|
||||||
assert.equal(describe("has:image"), "Messages that contain images");
|
assert.equal(describe("has:image"), "Messages that contain images");
|
||||||
|
@ -543,6 +545,7 @@ test("check_is_suggestions", ({override, mock_template}) => {
|
||||||
"is:dm",
|
"is:dm",
|
||||||
"is:starred",
|
"is:starred",
|
||||||
"is:mentioned",
|
"is:mentioned",
|
||||||
|
"is:followed",
|
||||||
"is:alerted",
|
"is:alerted",
|
||||||
"is:unread",
|
"is:unread",
|
||||||
"is:resolved",
|
"is:resolved",
|
||||||
|
@ -563,6 +566,7 @@ test("check_is_suggestions", ({override, mock_template}) => {
|
||||||
assert.equal(describe("is:alerted"), "Alerted messages");
|
assert.equal(describe("is:alerted"), "Alerted messages");
|
||||||
assert.equal(describe("is:unread"), "Unread messages");
|
assert.equal(describe("is:unread"), "Unread messages");
|
||||||
assert.equal(describe("is:resolved"), "Topics marked as resolved");
|
assert.equal(describe("is:resolved"), "Topics marked as resolved");
|
||||||
|
assert.equal(describe("is:followed"), "Followed topics");
|
||||||
|
|
||||||
query = "-i";
|
query = "-i";
|
||||||
suggestions = get_suggestions(query);
|
suggestions = get_suggestions(query);
|
||||||
|
@ -571,6 +575,7 @@ test("check_is_suggestions", ({override, mock_template}) => {
|
||||||
"-is:dm",
|
"-is:dm",
|
||||||
"-is:starred",
|
"-is:starred",
|
||||||
"-is:mentioned",
|
"-is:mentioned",
|
||||||
|
"-is:followed",
|
||||||
"-is:alerted",
|
"-is:alerted",
|
||||||
"-is:unread",
|
"-is:unread",
|
||||||
"-is:resolved",
|
"-is:resolved",
|
||||||
|
@ -583,12 +588,21 @@ test("check_is_suggestions", ({override, mock_template}) => {
|
||||||
assert.equal(describe("-is:alerted"), "Exclude alerted messages");
|
assert.equal(describe("-is:alerted"), "Exclude alerted messages");
|
||||||
assert.equal(describe("-is:unread"), "Exclude unread messages");
|
assert.equal(describe("-is:unread"), "Exclude unread messages");
|
||||||
assert.equal(describe("-is:resolved"), "Exclude topics marked as resolved");
|
assert.equal(describe("-is:resolved"), "Exclude topics marked as resolved");
|
||||||
|
assert.equal(describe("-is:followed"), "Exclude followed topics");
|
||||||
|
|
||||||
// operand suggestions follow.
|
// operand suggestions follow.
|
||||||
|
|
||||||
query = "is:";
|
query = "is:";
|
||||||
suggestions = get_suggestions(query);
|
suggestions = get_suggestions(query);
|
||||||
expected = ["is:dm", "is:starred", "is:mentioned", "is:alerted", "is:unread", "is:resolved"];
|
expected = [
|
||||||
|
"is:dm",
|
||||||
|
"is:starred",
|
||||||
|
"is:mentioned",
|
||||||
|
"is:followed",
|
||||||
|
"is:alerted",
|
||||||
|
"is:unread",
|
||||||
|
"is:resolved",
|
||||||
|
];
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
assert.deepEqual(suggestions.strings, expected);
|
||||||
|
|
||||||
query = "is:st";
|
query = "is:st";
|
||||||
|
|
|
@ -58,6 +58,7 @@ from zerver.lib.streams import (
|
||||||
get_web_public_streams_queryset,
|
get_web_public_streams_queryset,
|
||||||
)
|
)
|
||||||
from zerver.lib.topic_sqlalchemy import (
|
from zerver.lib.topic_sqlalchemy import (
|
||||||
|
get_followed_topic_condition_sa,
|
||||||
get_resolved_topic_condition_sa,
|
get_resolved_topic_condition_sa,
|
||||||
topic_column_sa,
|
topic_column_sa,
|
||||||
topic_match_sa,
|
topic_match_sa,
|
||||||
|
@ -405,6 +406,9 @@ class NarrowBuilder:
|
||||||
elif operand == "resolved":
|
elif operand == "resolved":
|
||||||
cond = get_resolved_topic_condition_sa()
|
cond = get_resolved_topic_condition_sa()
|
||||||
return query.where(maybe_negate(cond))
|
return query.where(maybe_negate(cond))
|
||||||
|
elif operand == "followed":
|
||||||
|
cond = get_followed_topic_condition_sa(self.user_profile.id)
|
||||||
|
return query.where(maybe_negate(cond))
|
||||||
raise BadNarrowOperatorError("unknown 'is' operand " + operand)
|
raise BadNarrowOperatorError("unknown 'is' operand " + operand)
|
||||||
|
|
||||||
_alphanum = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
_alphanum = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
|
@ -912,7 +916,7 @@ def ok_to_include_history(
|
||||||
# that's a property on the UserMessage table. There cannot be
|
# that's a property on the UserMessage table. There cannot be
|
||||||
# historical messages in these cases anyway.
|
# historical messages in these cases anyway.
|
||||||
for term in narrow:
|
for term in narrow:
|
||||||
if term["operator"] == "is" and term["operand"] not in {"resolved"}:
|
if term["operator"] == "is" and term["operand"] not in {"resolved", "followed"}:
|
||||||
include_history = False
|
include_history = False
|
||||||
|
|
||||||
return include_history
|
return include_history
|
||||||
|
|
|
@ -14,10 +14,15 @@ channels_operators: List[str] = ["channels", "streams"]
|
||||||
|
|
||||||
def check_narrow_for_events(narrow: Collection[NarrowTerm]) -> None:
|
def check_narrow_for_events(narrow: Collection[NarrowTerm]) -> None:
|
||||||
supported_operators = [*channel_operators, "topic", "sender", "is"]
|
supported_operators = [*channel_operators, "topic", "sender", "is"]
|
||||||
|
unsupported_is_operands = ["followed"]
|
||||||
for narrow_term in narrow:
|
for narrow_term in narrow:
|
||||||
operator = narrow_term.operator
|
operator = narrow_term.operator
|
||||||
if operator not in supported_operators:
|
if operator not in supported_operators:
|
||||||
raise JsonableError(_("Operator {operator} not supported.").format(operator=operator))
|
raise JsonableError(_("Operator {operator} not supported.").format(operator=operator))
|
||||||
|
if operator == "is" and narrow_term.operand in unsupported_is_operands:
|
||||||
|
raise JsonableError(
|
||||||
|
_("Operand {operand} not supported.").format(operand=narrow_term.operand)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NarrowPredicate(Protocol):
|
class NarrowPredicate(Protocol):
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from sqlalchemy.sql import ColumnElement, column, func, literal
|
from sqlalchemy.sql import ColumnElement, and_, column, func, literal, literal_column, select, table
|
||||||
from sqlalchemy.types import Boolean, Text
|
from sqlalchemy.types import Boolean, Text
|
||||||
|
|
||||||
from zerver.lib.topic import RESOLVED_TOPIC_PREFIX
|
from zerver.lib.topic import RESOLVED_TOPIC_PREFIX
|
||||||
|
from zerver.models import UserTopic
|
||||||
|
|
||||||
|
|
||||||
def topic_match_sa(topic_name: str) -> ColumnElement[Boolean]:
|
def topic_match_sa(topic_name: str) -> ColumnElement[Boolean]:
|
||||||
|
@ -18,3 +19,22 @@ def get_resolved_topic_condition_sa() -> ColumnElement[Boolean]:
|
||||||
|
|
||||||
def topic_column_sa() -> ColumnElement[Text]:
|
def topic_column_sa() -> ColumnElement[Text]:
|
||||||
return column("subject", Text)
|
return column("subject", Text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_followed_topic_condition_sa(user_id: int) -> ColumnElement[Boolean]:
|
||||||
|
follow_topic_cond = (
|
||||||
|
select([1])
|
||||||
|
.select_from(table("zerver_usertopic"))
|
||||||
|
.where(
|
||||||
|
and_(
|
||||||
|
literal_column("zerver_usertopic.user_profile_id") == literal(user_id),
|
||||||
|
literal_column("zerver_usertopic.visibility_policy")
|
||||||
|
== literal(UserTopic.VisibilityPolicy.FOLLOWED),
|
||||||
|
func.upper(literal_column("zerver_usertopic.topic_name"))
|
||||||
|
== func.upper(literal_column("zerver_message.subject")),
|
||||||
|
literal_column("zerver_usertopic.recipient_id")
|
||||||
|
== literal_column("zerver_message.recipient_id"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).exists()
|
||||||
|
return follow_topic_cond
|
||||||
|
|
|
@ -270,6 +270,20 @@ class NarrowBuilderTest(ZulipTestCase):
|
||||||
term = dict(operator="is", operand="resolved", negated=True)
|
term = dict(operator="is", operand="resolved", negated=True)
|
||||||
self._do_add_term_test(term, "WHERE (subject NOT LIKE %(subject_1)s || '%%'")
|
self._do_add_term_test(term, "WHERE (subject NOT LIKE %(subject_1)s || '%%'")
|
||||||
|
|
||||||
|
def test_add_term_using_is_operator_for_followed_topics(self) -> None:
|
||||||
|
term = dict(operator="is", operand="followed", negated=False)
|
||||||
|
self._do_add_term_test(
|
||||||
|
term,
|
||||||
|
"EXISTS (SELECT 1 \nFROM zerver_usertopic \nWHERE zerver_usertopic.user_profile_id = %(param_1)s AND zerver_usertopic.visibility_policy = %(param_2)s AND upper(zerver_usertopic.topic_name) = upper(zerver_message.subject) AND zerver_usertopic.recipient_id = zerver_message.recipient_id)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_term_using_is_operator_for_negated_followed_topics(self) -> None:
|
||||||
|
term = dict(operator="is", operand="followed", negated=True)
|
||||||
|
self._do_add_term_test(
|
||||||
|
term,
|
||||||
|
"NOT (EXISTS (SELECT 1 \nFROM zerver_usertopic \nWHERE zerver_usertopic.user_profile_id = %(param_1)s AND zerver_usertopic.visibility_policy = %(param_2)s AND upper(zerver_usertopic.topic_name) = upper(zerver_message.subject) AND zerver_usertopic.recipient_id = zerver_message.recipient_id))",
|
||||||
|
)
|
||||||
|
|
||||||
def test_add_term_using_non_supported_operator_should_raise_error(self) -> None:
|
def test_add_term_using_non_supported_operator_should_raise_error(self) -> None:
|
||||||
term = dict(operator="is", operand="non_supported")
|
term = dict(operator="is", operand="non_supported")
|
||||||
self.assertRaises(BadNarrowOperatorError, self._build_query, term)
|
self.assertRaises(BadNarrowOperatorError, self._build_query, term)
|
||||||
|
@ -951,6 +965,8 @@ class NarrowLibraryTest(ZulipTestCase):
|
||||||
def test_build_narrow_predicate_invalid(self) -> None:
|
def test_build_narrow_predicate_invalid(self) -> None:
|
||||||
with self.assertRaises(JsonableError):
|
with self.assertRaises(JsonableError):
|
||||||
build_narrow_predicate([NarrowTerm(operator="invalid_operator", operand="operand")])
|
build_narrow_predicate([NarrowTerm(operator="invalid_operator", operand="operand")])
|
||||||
|
with self.assertRaises(JsonableError):
|
||||||
|
build_narrow_predicate([NarrowTerm(operator="is", operand="followed")])
|
||||||
|
|
||||||
def test_is_spectator_compatible(self) -> None:
|
def test_is_spectator_compatible(self) -> None:
|
||||||
self.assertTrue(is_spectator_compatible([]))
|
self.assertTrue(is_spectator_compatible([]))
|
||||||
|
@ -4584,6 +4600,37 @@ class MessageHasKeywordsTest(ZulipTestCase):
|
||||||
self.assert_length(messages, 1)
|
self.assert_length(messages, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageIsTest(ZulipTestCase):
|
||||||
|
def test_message_is_followed(self) -> None:
|
||||||
|
self.login("iago")
|
||||||
|
is_followed_narrow = orjson.dumps([dict(operator="is", operand="followed")]).decode()
|
||||||
|
|
||||||
|
# Sending a message in a topic that isn't followed by the user.
|
||||||
|
msg_id = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="hey")
|
||||||
|
result = self.client_get(
|
||||||
|
"/json/messages",
|
||||||
|
dict(narrow=is_followed_narrow, anchor=msg_id, num_before=0, num_after=0),
|
||||||
|
)
|
||||||
|
messages = self.assert_json_success(result)["messages"]
|
||||||
|
self.assert_length(messages, 0)
|
||||||
|
|
||||||
|
stream_id = self.get_stream_id("Denmark", self.example_user("hamlet").realm)
|
||||||
|
|
||||||
|
# Following the topic.
|
||||||
|
payload = {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"topic": "hey",
|
||||||
|
"visibility_policy": int(UserTopic.VisibilityPolicy.FOLLOWED),
|
||||||
|
}
|
||||||
|
self.client_post("/json/user_topics", payload)
|
||||||
|
result = self.client_get(
|
||||||
|
"/json/messages",
|
||||||
|
dict(narrow=is_followed_narrow, anchor=msg_id, num_before=0, num_after=0),
|
||||||
|
)
|
||||||
|
messages = self.assert_json_success(result)["messages"]
|
||||||
|
self.assert_length(messages, 1)
|
||||||
|
|
||||||
|
|
||||||
class MessageVisibilityTest(ZulipTestCase):
|
class MessageVisibilityTest(ZulipTestCase):
|
||||||
def test_update_first_visible_message_id(self) -> None:
|
def test_update_first_visible_message_id(self) -> None:
|
||||||
Message.objects.all().delete()
|
Message.objects.all().delete()
|
||||||
|
|
Loading…
Reference in New Issue