/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/narrow_state.ts
387 строк
13 KB
Anders Kaseorg
eslint: Fix unicorn/prefer-smaller-scope.
29 июл 2026, 02:35
29 июл 2026, 02:35
9124c08
Код
Авторство
О чём код?
import assert from "minimalistic-assert"; import {Filter} from "./filter.ts"; import * as inbox_util from "./inbox_util.ts"; import * as message_lists from "./message_lists.ts"; import {page_params} from "./page_params.ts"; import * as people from "./people.ts"; import type {NarrowCanonicalTerm, NarrowTerm} from "./state_data.ts"; import * as stream_data from "./stream_data.ts"; import type {StreamSubscription} from "./sub_store.ts"; import * as unread from "./unread.ts"; import * as util from "./util.ts"; export function filter(): Filter | undefined { // We use Filter objects for message views as well as the list of // topics channel view. // // TODO: Some renaming/refactoring to put this in a separate // module from the rest of this file, which is all about message // views, would be valuable. if (inbox_util.is_visible()) { return inbox_util.filter; } // `Recent Conversations` returns undefined; return message_lists.current?.data.filter; } export function search_terms(current_filter: Filter | undefined = filter()): NarrowCanonicalTerm[] { if (current_filter === undefined) { if (page_params.narrow !== undefined) { current_filter = new Filter(page_params.narrow); } else { current_filter = new Filter([]); } } const non_search_operators = new Set(["with"]); return current_filter.terms().filter((term) => !non_search_operators.has(term.operator)); } export function is_search_view(current_filter: Filter | undefined = filter()): boolean { if (current_filter && !current_filter.contains_no_partial_conversations()) { return true; } return false; } export function is_message_feed_visible(): boolean { // It's important that `message_lists.current` is the // source of truth for this since during the initial app load, // `message_lists.current` is `undefined` and we don't want // to return `true` if we haven't loaded the message feed yet. return message_lists.current !== undefined; } /* Search terms we should send to the server. */ export function public_search_terms( current_filter: Filter | undefined = filter(), ): NarrowTerm[] | undefined { if (current_filter === undefined) { return undefined; } return current_filter.public_terms(); } // Collect terms which appear only once into a map, // and discard those which appear more than once. // Returns `NarrowTerm` so that type is preserved for further use. function collect_single(terms: NarrowTerm[]): Map<NarrowTerm["operator"], NarrowTerm> { const seen = new Set<NarrowTerm["operator"]>(); const result = new Map<NarrowTerm["operator"], NarrowTerm>(); for (const term of terms) { const key = term.operator; if (seen.has(key)) { result.delete(key); } else { result.set(key, term); seen.add(key); } } return result; } // Modify default compose parameters (stream etc.) based on // the current narrowed view. // // This logic is here and not in the 'compose' module because // it will get more complicated as we add things to the narrow // search term language. export function set_compose_defaults(): { stream_id?: number; topic?: string; private_message_recipient_ids?: number[]; } { const opts: {stream_id?: number; topic?: string; private_message_recipient_ids?: number[]} = {}; const single = collect_single(search_terms()); // Set the stream, topic, and/or direct message recipient // if they are uniquely specified in the narrow view. if (single.has("channel")) { // Only set opts.stream_id if it is a valid stream ID. const narrow_stream_id = stream_id(filter(), true); if (narrow_stream_id !== undefined) { opts.stream_id = narrow_stream_id; } } const topic_term = single.get("topic"); if (topic_term !== undefined) { assert(topic_term.operator === "topic"); opts.topic = topic_term.operand; } const dm_term = single.get("dm"); if ( // Check for typescript to understand operand type. dm_term?.operator === "dm" && people.is_valid_bulk_user_ids_for_compose(dm_term.operand, true) ) { opts.private_message_recipient_ids = dm_term.operand; } return opts; } export function stream_id( current_filter: Filter | undefined = filter(), // If true, we'll return undefined if the filter contains a // stream_id, but that stream ID is not present in stream_data // (whether because it's an invalid channel ID, or because the // channel is not accessible to this user). only_valid_id = false, ): number | undefined { if (current_filter === undefined) { return undefined; } const channel_terms = current_filter.terms_with_operator("channel"); if (channel_terms.length === 1) { const channel_operand = channel_terms[0]?.operand; if (channel_operand !== undefined) { const id = Number.parseInt(channel_operand, 10); if (!Number.isNaN(id)) { return only_valid_id ? stream_data.get_sub_by_id(id)?.stream_id : id; } } } return undefined; } export function stream_name(current_filter: Filter | undefined = filter()): string | undefined { const id = stream_id(current_filter); if (id === undefined) { return undefined; } return stream_data.get_sub_by_id(id)?.name; } export function stream_sub( current_filter: Filter | undefined = filter(), ): StreamSubscription | undefined { const id = stream_id(current_filter); if (id === undefined) { return undefined; } return stream_data.get_sub_by_id(id); } export function topic(current_filter: Filter | undefined = filter()): string | undefined { if (current_filter === undefined) { return undefined; } const terms = current_filter.terms_with_operator("topic"); if (terms.length === 1) { return terms[0]!.operand; } return undefined; } export function pm_ids(current_filter: Filter | undefined = filter()): number[] | undefined { if (current_filter === undefined) { return undefined; } const terms = current_filter.terms_with_operator("dm"); if (terms.length !== 1) { return undefined; } // If you are narrowed to a group direct message with users 4, 5, and 99, // this will return [4,5,99]. const user_ids = util.the(terms).operand; if (user_ids.length === 0 || !people.is_valid_user_ids(user_ids)) { return undefined; } return util.the(terms).operand; } export function pm_ids_string(current_filter: Filter | undefined = filter()): string | undefined { const ids = pm_ids(current_filter); return ids ? String(ids) : undefined; } export function pm_ids_set(filter?: Filter): Set<number> { const ids = pm_ids(filter) ?? []; return new Set(ids); } // We expect get_first_unread_info and therefore _possible_unread_message_ids // to always be called with a filter from a message list. export let get_first_unread_info = ( message_list_filter: Filter, ): {flavor: "cannot_compute" | "not_found"} | {flavor: "found"; msg_id: number} => { const cannot_compute_response: {flavor: "cannot_compute"} = {flavor: "cannot_compute"}; if (!message_list_filter.can_apply_locally()) { // For things like search queries, where the server has info // that the client isn't privy to, we need to wait for the // server to give us a definitive list of messages before // deciding where we'll move the selection. return cannot_compute_response; } const unread_ids = _possible_unread_message_ids(message_list_filter); if (unread_ids === undefined) { // _possible_unread_message_ids() only works for certain narrows return cannot_compute_response; } const msg_id = message_list_filter.first_valid_id_from(unread_ids); if (msg_id === undefined) { return { flavor: "not_found", }; } return { flavor: "found", msg_id, }; }; export function rewire_get_first_unread_info(value: typeof get_first_unread_info): void { get_first_unread_info = value; } export let _possible_unread_message_ids = (message_list_filter: Filter): number[] | undefined => { // This function currently only returns valid results for // certain types of narrows, mostly left sidebar narrows. // For more complicated narrows we may return undefined. // // If we do return a result, it will be a subset of unread // message ids but possibly a superset of unread message ids // that match our filter. // For the `with` operator, we can only correctly compute the // correct channel/topic for lookup unreads in if we either // have the message in our local cache, or we know the filter // has already been updated for potentially moved messages. // // The code path that needs this function is never called in // the `with` code path, but for safety, we assert that // assumption is not violated. // // If we need to change that assumption, we can try looking up the // target message in message_store, but would need to return // undefined if the target message is not available. assert(!message_list_filter.requires_adjustment_for_moved_with_target); if ( message_list_filter.can_bucket_by("channel", "topic", "with") || message_list_filter.can_bucket_by("channel", "topic") ) { const filter_stream_id = stream_id(message_list_filter, true); const topic_name = topic(message_list_filter); if (filter_stream_id === undefined || topic_name === undefined) { return []; } return unread.get_msg_ids_for_topic(filter_stream_id, topic_name); } if (message_list_filter.can_bucket_by("channel")) { const filter_stream_id = stream_id(message_list_filter, true); if (filter_stream_id === undefined) { return []; } return unread.get_msg_ids_for_stream(filter_stream_id); } if ( message_list_filter.can_bucket_by("dm", "with") || message_list_filter.can_bucket_by("dm") ) { const filter_pm_string = pm_ids_string(message_list_filter); if (filter_pm_string === undefined) { return []; } return unread.get_msg_ids_for_user_ids_string(filter_pm_string); } if (message_list_filter.can_bucket_by("is-dm")) { return unread.get_msg_ids_for_private(); } if (message_list_filter.can_bucket_by("is-mentioned")) { return unread.get_msg_ids_for_mentions(); } if (message_list_filter.can_bucket_by("is-starred")) { return unread.get_msg_ids_for_starred(); } if (message_list_filter.can_bucket_by("sender")) { // TODO: see #9352 to make this more efficient return unread.get_all_msg_ids(); } if (message_list_filter.can_apply_locally()) { return unread.get_all_msg_ids(); } return undefined; }; export function rewire__possible_unread_message_ids( value: typeof _possible_unread_message_ids, ): void { _possible_unread_message_ids = value; } // Are we narrowed to direct messages: the direct message feed or a // specific direct message conversation. export function narrowed_to_pms(current_filter: Filter | undefined = filter()): boolean { if (current_filter === undefined) { return false; } return current_filter.has_operator("dm") || current_filter.has_operand("is", "dm"); } export function narrowed_by_pm_reply(current_filter: Filter | undefined = filter()): boolean { if (current_filter === undefined) { return false; } const terms = current_filter.terms().filter((term) => term.operator !== "with"); return terms.length === 1 && current_filter.has_operator("dm"); } export function narrowed_by_topic_reply(current_filter: Filter | undefined = filter()): boolean { if (current_filter === undefined) { return false; } const terms = current_filter.terms().filter((term) => term.operator !== "with"); return ( terms.length === 2 && current_filter.terms_with_operator("channel").length === 1 && current_filter.terms_with_operator("topic").length === 1 ); } // We auto-reply under certain conditions, namely when you're narrowed // to a 1:1 or group direct message conversation, and when you're // narrowed to some stream/topic pair. export function narrowed_by_reply(filter?: Filter): boolean { return narrowed_by_pm_reply(filter) || narrowed_by_topic_reply(filter); } export function narrowed_by_stream_reply(current_filter: Filter | undefined = filter()): boolean { if (current_filter === undefined) { return false; } const terms = current_filter.terms(); return terms.length === 1 && current_filter.terms_with_operator("channel").length === 1; } export function narrowed_to_stream_id(stream_id_to_check: number, filter?: Filter): boolean { const current_stream_id = stream_id(filter); if (current_stream_id === undefined) { return false; } return stream_id_to_check === current_stream_id; }