filter: Implement web public channels filter for suggestions.

We already had web-public support in narrow by_channels.

This commit implements the web-public support for actual user
search.
This commit is contained in:
Pratik Chanda
2025-06-16 17:03:38 +05:30
committed by Tim Abbott
parent aa9aa2160b
commit 40ff1220ef
7 changed files with 147 additions and 35 deletions

View File

@@ -94,6 +94,9 @@ additional messages.
* `channels:public`: Search messages in all * `channels:public`: Search messages in all
[public](/help/channel-permissions#public-channels) and [public](/help/channel-permissions#public-channels) and
[web-public](/help/channel-permissions#web-public-channels) channels. [web-public](/help/channel-permissions#web-public-channels) channels.
* `channels:web-public`: Search messages in all
[web-public](/help/change-the-privacy-of-a-channel) channels in the organization,
including channels you are not subscribed to.
* `channel:design`: Search all messages in **#design**, including messages sent * `channel:design`: Search all messages in **#design**, including messages sent
before you were a subscriber. before you were a subscriber.

View File

@@ -75,6 +75,8 @@ type ValidOrInvalidUser =
| {valid_user: true; user_pill_context: UserPillItem} | {valid_user: true; user_pill_context: UserPillItem}
| {valid_user: false; operand: string}; | {valid_user: false; operand: string};
const channels_operands = new Set(["public", "web-public"]);
function zephyr_stream_name_match( function zephyr_stream_name_match(
message: Message & {type: "stream"}, message: Message & {type: "stream"},
stream_name: string, stream_name: string,
@@ -217,7 +219,14 @@ function message_matches_search_term(message: Message, operator: string, operand
return false; return false;
} }
const stream_privacy_policy = stream_data.get_stream_privacy_policy(message.stream_id); const stream_privacy_policy = stream_data.get_stream_privacy_policy(message.stream_id);
return ["public", "web-public"].includes(stream_privacy_policy) && operand === "public"; switch (operand) {
case "public":
return ["public", "web-public"].includes(stream_privacy_policy);
case "web-public":
return stream_privacy_policy === "web-public";
default:
return false;
}
} }
case "topic": case "topic":
@@ -583,7 +592,7 @@ export class Filter {
return stream_data.get_sub_by_id_string(term.operand) !== undefined; return stream_data.get_sub_by_id_string(term.operand) !== undefined;
case "channels": case "channels":
case "streams": case "streams":
return term.operand === "public"; return channels_operands.has(term.operand);
case "topic": case "topic":
return true; return true;
case "sender": case "sender":
@@ -655,6 +664,7 @@ export class Filter {
const levels = [ const levels = [
"in", "in",
"channels-public", "channels-public",
"channels-web-public",
"channel", "channel",
"topic", "topic",
"dm", "dm",
@@ -709,7 +719,7 @@ export class Filter {
case "channel": case "channel":
return verb + "messages in a channel"; return verb + "messages in a channel";
case "channels": case "channels":
return verb + "channels"; return verb + "channel type";
case "near": case "near":
return verb + "messages around"; return verb + "messages around";
@@ -816,10 +826,10 @@ export class Filter {
}; };
} }
} }
if (canonicalized_operator === "channels" && operand === "public") { if (canonicalized_operator === "channels" && channels_operands.has(operand)) {
return { return {
type: "plain_text", type: "plain_text",
content: this.describe_public_channels(term.negated ?? false), content: this.describe_channels_operator(term.negated ?? false, operand),
}; };
} }
const prefix_for_operator = Filter.operator_to_prefix( const prefix_for_operator = Filter.operator_to_prefix(
@@ -883,13 +893,19 @@ export class Filter {
return [...parts, ...more_parts]; return [...parts, ...more_parts];
} }
static describe_public_channels(negated: boolean): string { static describe_channels_operator(negated: boolean, operand: string): string {
const possible_prefix = negated ? "exclude " : ""; const possible_prefix = negated ? "exclude " : "";
if (page_params.is_spectator || current_user.is_guest) { assert(channels_operands.has(operand));
if ((page_params.is_spectator || current_user.is_guest) && operand === "public") {
return possible_prefix + "all public channels that you can view"; return possible_prefix + "all public channels that you can view";
} }
switch (operand) {
case "web-public":
return possible_prefix + "all web-public channels";
default:
return possible_prefix + "all public channels"; return possible_prefix + "all public channels";
} }
}
static search_description_as_html( static search_description_as_html(
terms: NarrowTerm[], terms: NarrowTerm[],
@@ -1260,6 +1276,9 @@ export class Filter {
if (_.isEqual(term_types, ["channels-public"])) { if (_.isEqual(term_types, ["channels-public"])) {
return true; return true;
} }
if (_.isEqual(term_types, ["channels-web-public"])) {
return true;
}
if (_.isEqual(term_types, ["sender"])) { if (_.isEqual(term_types, ["sender"])) {
return true; return true;
} }
@@ -1329,6 +1348,8 @@ export class Filter {
return "/#narrow/is/mentioned"; return "/#narrow/is/mentioned";
case "channels-public": case "channels-public":
return "/#narrow/channels/public"; return "/#narrow/channels/public";
case "channels-web-public":
return "/#narrow/channels/web-public";
case "dm": case "dm":
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":
@@ -1502,6 +1523,8 @@ export class Filter {
}); });
} }
return $t({defaultMessage: "Messages in all public channels"}); return $t({defaultMessage: "Messages in all public channels"});
case "channels-web-public":
return $t({defaultMessage: "Messages in all web-public channels"});
case "is-starred": case "is-starred":
return $t({defaultMessage: "Starred messages"}); return $t({defaultMessage: "Starred messages"});
case "is-mentioned": case "is-mentioned":
@@ -1894,6 +1917,8 @@ export class Filter {
"not-is-resolved", "not-is-resolved",
"channels-public", "channels-public",
"not-channels-public", "not-channels-public",
"channels-web-public",
"not-channels-web-public",
"is-muted", "is-muted",
"not-is-muted", "not-is-muted",
"in-home", "in-home",

View File

@@ -588,29 +588,40 @@ function get_special_filter_suggestions(
} }
function get_channels_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] { function get_channels_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
let search_string = "channels:public"; if (last.operator !== "channels") {
// show "channels:public" option for users who return [];
// have "streams" in their muscle memory
if (last.operator === "search" && common.phrase_match(last.operand, "streams")) {
search_string = "streams:public";
} }
let description_html = "all public channels"; const incompatible_patterns = [
const suggestions: SuggestionAndIncompatiblePatterns[] = [
{
search_string,
description_html,
incompatible_patterns: [
{operator: "is", operand: "dm"}, {operator: "is", operand: "dm"},
{operator: "channel"}, {operator: "channel"},
{operator: "dm-including"}, {operator: "dm-including"},
{operator: "dm"}, {operator: "dm"},
{operator: "in"}, {operator: "in"},
{operator: "channels"}, {operator: "channels"},
],
},
]; ];
const public_channels_search_string = "channels:public";
const web_public_channels_search_string = "channels:web-public";
const suggestions: SuggestionAndIncompatiblePatterns[] = [];
if (!page_params.is_spectator) {
suggestions.push({
search_string: public_channels_search_string,
description_html: "all public channels",
incompatible_patterns,
});
}
if (stream_data.realm_has_web_public_streams()) {
suggestions.push({
search_string: web_public_channels_search_string,
description_html: "all web-public channels",
incompatible_patterns,
});
}
return get_special_filter_suggestions(last, terms, suggestions); return get_special_filter_suggestions(last, terms, suggestions);
} }
function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] { function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
let suggestions: SuggestionAndIncompatiblePatterns[]; let suggestions: SuggestionAndIncompatiblePatterns[];
if (page_params.is_spectator) { if (page_params.is_spectator) {
@@ -804,9 +815,10 @@ function get_operator_suggestions(last: NarrowTerm): Suggestion[] {
let choices; let choices;
if (last.operator === "") { if (last.operator === "") {
choices = ["channel", "stream"]; choices = ["channels", "channel", "streams", "stream"];
} else { } else {
choices = [ choices = [
"channels",
"channel", "channel",
"topic", "topic",
"dm", "dm",
@@ -815,6 +827,7 @@ function get_operator_suggestions(last: NarrowTerm): Suggestion[] {
"near", "near",
"from", "from",
"pm-with", "pm-with",
"streams",
"stream", "stream",
]; ];
} }
@@ -832,6 +845,11 @@ function get_operator_suggestions(last: NarrowTerm): Suggestion[] {
if (choice === "stream") { if (choice === "stream") {
choice = "channel"; choice = "channel";
} }
// Map results for "channels:" operator for users
// who have "streams" in their muscle memory.
if (choice === "streams") {
choice = "channels";
}
const op = [{operator: choice, operand: "", negated}]; const op = [{operator: choice, operand: "", negated}];
return format_as_suggestion(op, true); return format_as_suggestion(op, true);
}); });
@@ -1066,6 +1084,7 @@ export function get_search_result(
if (page_params.is_spectator) { if (page_params.is_spectator) {
filterers = [ filterers = [
get_channels_filter_suggestions,
get_operator_suggestions, get_operator_suggestions,
get_is_filter_suggestions, get_is_filter_suggestions,
get_channel_suggestions, get_channel_suggestions,

View File

@@ -378,6 +378,10 @@ export function get_archived_subs(): StreamSubscription[] {
return [...stream_info.values()].filter((sub) => sub.is_archived); return [...stream_info.values()].filter((sub) => sub.is_archived);
} }
export function realm_has_web_public_streams(): boolean {
return realm_web_public_stream_ids.size > 0;
}
export function muted_stream_ids(): number[] { export function muted_stream_ids(): number[] {
return subscribed_subs() return subscribed_subs()
.filter((sub) => sub.is_muted) .filter((sub) => sub.is_muted)

View File

@@ -70,6 +70,12 @@
{{/if}} {{/if}}
</td> </td>
</tr> </tr>
<tr>
<td class="operator">channels:web-public</td>
<td class="definition">
{{t 'Search all web-public channels.'}}
</td>
</tr>
<tr> <tr>
<td class="operator">sender:<span class="operator_value">user</span></td> <td class="operator">sender:<span class="operator_value">user</span></td>
<td class="definition"> <td class="definition">

View File

@@ -1092,6 +1092,7 @@ test("predicate_basics", ({override}) => {
// a subscription, but these should still match by channel name. // a subscription, but these should still match by channel name.
const old_sub_id = new_stream_id(); const old_sub_id = new_stream_id();
const private_sub_id = new_stream_id(); const private_sub_id = new_stream_id();
const web_public_sub_id = new_stream_id();
const old_sub = { const old_sub = {
name: "old-subscription", name: "old-subscription",
stream_id: old_sub_id, stream_id: old_sub_id,
@@ -1106,8 +1107,16 @@ test("predicate_basics", ({override}) => {
invite_only: true, invite_only: true,
is_web_public: false, is_web_public: false,
}; };
const web_public_sub = {
name: "web-public-subscription",
stream_id: web_public_sub_id,
subscribed: false,
invite_only: false,
is_web_public: true,
};
stream_data.add_sub(old_sub); stream_data.add_sub(old_sub);
stream_data.add_sub(private_sub); stream_data.add_sub(private_sub);
stream_data.add_sub(web_public_sub);
predicate = get_predicate([ predicate = get_predicate([
["channel", old_sub_id.toString()], ["channel", old_sub_id.toString()],
["topic", "Bar"], ["topic", "Bar"],
@@ -1129,6 +1138,14 @@ test("predicate_basics", ({override}) => {
predicate = get_predicate([["channels", "public"]]); predicate = get_predicate([["channels", "public"]]);
assert.ok(predicate({type: stream_message, stream_id: old_sub_id})); assert.ok(predicate({type: stream_message, stream_id: old_sub_id}));
assert.ok(!predicate({type: stream_message, stream_id: private_sub_id})); assert.ok(!predicate({type: stream_message, stream_id: private_sub_id}));
assert.ok(predicate({type: stream_message, stream_id: web_public_sub_id}));
predicate = get_predicate([["channels", "web-public"]]);
assert.ok(predicate({type: stream_message, stream_id: web_public_sub_id}));
assert.ok(!predicate({type: stream_message, stream_id: old_sub_id}));
predicate = get_predicate([["channels", "bogus"]]);
assert.ok(!predicate({type: stream_message, stream_id: old_sub_id}));
predicate = get_predicate([["is", "starred"]]); predicate = get_predicate([["is", "starred"]]);
assert.ok(predicate({starred: true})); assert.ok(predicate({starred: true}));
@@ -1696,10 +1713,26 @@ test("describe", ({mock_template, override}) => {
string = "exclude all public channels"; string = "exclude all public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public"}];
string = "all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public", negated: true}];
string = "exclude all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
page_params.is_spectator = true; page_params.is_spectator = true;
narrow = [{operator: "channels", operand: "public"}]; narrow = [{operator: "channels", operand: "public"}];
string = "all public channels that you can view"; string = "all public channels that you can view";
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public"}];
string = "all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public", negated: true}];
string = "exclude all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
page_params.is_spectator = false; page_params.is_spectator = false;
const devel_id = new_stream_id(); const devel_id = new_stream_id();
@@ -2349,6 +2382,7 @@ test("navbar_helpers", ({override}) => {
const is_resolved = [{operator: "is", operand: "resolved"}]; const is_resolved = [{operator: "is", operand: "resolved"}];
const is_followed = [{operator: "is", operand: "followed"}]; const is_followed = [{operator: "is", operand: "followed"}];
const channels_public = [{operator: "channels", operand: "public"}]; const channels_public = [{operator: "channels", operand: "public"}];
const channels_web_public = [{operator: "channels", operand: "web-public"}];
const channel_topic_terms = [ const channel_topic_terms = [
{operator: "channel", operand: foo_stream_id.toString()}, {operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"}, {operator: "topic", operand: "bar"},
@@ -2505,6 +2539,13 @@ test("navbar_helpers", ({override}) => {
title: "translated: Messages in all public channels", title: "translated: Messages in all public channels",
redirect_url_with_search: "/#narrow/channels/public", redirect_url_with_search: "/#narrow/channels/public",
}, },
{
terms: channels_web_public,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages in all web-public channels",
redirect_url_with_search: "/#narrow/channels/web-public",
},
{ {
terms: channel_term, terms: channel_term,
is_common_narrow: true, is_common_narrow: true,

View File

@@ -103,10 +103,14 @@ test("basic_get_suggestions", ({override}) => {
test("basic_get_suggestions_for_spectator", () => { test("basic_get_suggestions_for_spectator", () => {
page_params.is_spectator = true; page_params.is_spectator = true;
const web_public_id = new_stream_id();
const sub = {name: "Web public", stream_id: web_public_id, is_web_public: true};
stream_data.add_sub(sub);
const query = ""; let query = "";
const suggestions = get_suggestions(query); let suggestions = get_suggestions(query);
assert.deepEqual(suggestions.strings, [ assert.deepEqual(suggestions.strings, [
"channels:",
"channel:", "channel:",
"is:resolved", "is:resolved",
"-is:resolved", "-is:resolved",
@@ -115,6 +119,11 @@ test("basic_get_suggestions_for_spectator", () => {
"has:attachment", "has:attachment",
"has:reaction", "has:reaction",
]); ]);
stream_data.delete_sub(sub.stream_id);
query = "channels:";
suggestions = get_suggestions(query);
assert.deepEqual(suggestions.strings, []);
page_params.is_spectator = false; page_params.is_spectator = false;
}); });
@@ -384,13 +393,18 @@ test("empty_query_suggestions", () => {
const devel_id = new_stream_id(); const devel_id = new_stream_id();
const office_id = new_stream_id(); const office_id = new_stream_id();
stream_data.add_sub({stream_id: devel_id, name: "devel", subscribed: true}); stream_data.add_sub({
stream_id: devel_id,
name: "devel",
subscribed: true,
is_web_public: true,
});
stream_data.add_sub({stream_id: office_id, name: "office", subscribed: true}); stream_data.add_sub({stream_id: office_id, name: "office", subscribed: true});
const suggestions = get_suggestions(query); const suggestions = get_suggestions(query);
const expected = [ const expected = [
"channels:public", "channels:",
"channel:", "channel:",
"is:dm", "is:dm",
"is:starred", "is:starred",
@@ -578,7 +592,7 @@ test("check_is_suggestions", ({override, mock_template}) => {
// but shows html description used for "channels:public" // but shows html description used for "channels:public"
query = "st"; query = "st";
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
expected = ["st", "streams:public", "channel:", "is:starred"]; expected = ["st", "channels:", "channel:", "is:starred"];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
query = "channel:66 has:link is:sta"; query = "channel:66 has:link is:sta";
@@ -992,12 +1006,12 @@ test("operator_suggestions", ({override, mock_template}) => {
query = "ch"; query = "ch";
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
expected = ["ch", "channels:public", "channel:"]; expected = ["ch", "channels:", "channel:"];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
query = "-s"; query = "-s";
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
expected = ["-s", "-sender:", "-channel:", "-sender:myself@zulip.com"]; expected = ["-s", "-sender:", "-channels:", "-channel:", "-sender:myself@zulip.com"];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
// 66 is a misc channel id. // 66 is a misc channel id.