import $ from "jquery"; import _ from "lodash"; import * as compose_banner from "./compose_banner"; import * as hash_util from "./hash_util"; import * as loading from "./loading"; import * as message_fetch from "./message_fetch"; import * as message_lists from "./message_lists"; import * as message_viewport from "./message_viewport"; import * as narrow_banner from "./narrow_banner"; import * as narrow_state from "./narrow_state"; import * as recent_topics_util from "./recent_topics_util"; import * as unread from "./unread"; import * as unread_ops from "./unread_ops"; import * as unread_ui from "./unread_ui"; let actively_scrolling = false; // Tracks whether the next scroll that will complete is initiated by // code, not the user, and thus should avoid moving the selected // message. let update_selection_on_next_scroll = true; export function suppress_selection_update_on_next_scroll() { update_selection_on_next_scroll = false; } // Whether a keyboard shortcut is triggering a message feed scroll event. let keyboard_triggered_current_scroll = false; export function mark_keyboard_triggered_current_scroll() { keyboard_triggered_current_scroll = true; } let loading_older_messages_indicator_showing = false; let loading_newer_messages_indicator_showing = false; export function show_loading_older() { if (!loading_older_messages_indicator_showing) { loading.make_indicator($("#loading_older_messages_indicator"), {abs_positioned: true}); loading_older_messages_indicator_showing = true; } } export function hide_loading_older() { if (loading_older_messages_indicator_showing) { loading.destroy_indicator($("#loading_older_messages_indicator")); loading_older_messages_indicator_showing = false; } } export function show_loading_newer() { if (!loading_newer_messages_indicator_showing) { $(".bottom-messages-logo").show(); loading.make_indicator($("#loading_newer_messages_indicator"), {abs_positioned: true}); loading_newer_messages_indicator_showing = true; } } export function hide_loading_newer() { if (loading_newer_messages_indicator_showing) { $(".bottom-messages-logo").hide(); loading.destroy_indicator($("#loading_newer_messages_indicator")); loading_newer_messages_indicator_showing = false; } } export function hide_indicators() { hide_loading_older(); hide_loading_newer(); } export function show_history_limit_notice() { $(".top-messages-logo").hide(); $(".history-limited-box").show(); narrow_banner.hide_empty_narrow_message(); } export function hide_history_limit_notice() { $(".top-messages-logo").show(); $(".history-limited-box").hide(); } export function hide_end_of_results_notice() { $(".all-messages-search-caution").hide(); } export function show_end_of_results_notice() { $(".all-messages-search-caution").show(); // Set the link to point to this search with streams:public added. // Note that element we adjust is not visible to spectators. const operators = narrow_state.filter().operators(); const update_hash = hash_util.search_public_streams_notice_url(operators); $(".all-messages-search-caution a.search-shared-history").attr("href", update_hash); } export function update_top_of_narrow_notices(msg_list) { // Assumes that the current state is all notices hidden (i.e. this // will not hide a notice that should not be there) if (msg_list !== message_lists.current) { return; } if ( msg_list.data.fetch_status.has_found_oldest() && message_lists.current !== message_lists.home ) { const filter = narrow_state.filter(); if (filter === undefined && !narrow_state.is_message_feed_visible()) { // user moved away from the narrow / filter to recent topics. return; } // Potentially display the notice that lets users know // that not all messages were searched. One could // imagine including `filter.is_search()` in these // conditions, but there's a very legitimate use case // for moderation of searching for all messages sent // by a potential spammer user. if ( !filter.contains_only_private_messages() && !filter.includes_full_stream_history() && !filter.is_personal_filter() ) { show_end_of_results_notice(); } } if (msg_list.data.fetch_status.history_limited()) { show_history_limit_notice(); } } export function hide_top_of_narrow_notices() { hide_end_of_results_notice(); hide_history_limit_notice(); } let hide_scroll_to_bottom_timer; export function hide_scroll_to_bottom() { const $show_scroll_to_bottom_button = $("#scroll-to-bottom-button-container"); if (message_viewport.bottom_message_visible() || message_lists.current.empty()) { // If last message is visible, just hide the // scroll to bottom button. $show_scroll_to_bottom_button.removeClass("show"); return; } // Wait before hiding to allow user time to click on the button. hide_scroll_to_bottom_timer = setTimeout(() => { // Don't hide if user is hovered on it. if ( !narrow_state.narrowed_by_topic_reply() && !$show_scroll_to_bottom_button.get(0).matches(":hover") ) { $show_scroll_to_bottom_button.removeClass("show"); } }, 3000); } export function show_scroll_to_bottom_button() { if (message_viewport.bottom_message_visible()) { // Only show scroll to bottom button when // last message is not visible in the // current scroll position. return; } clearTimeout(hide_scroll_to_bottom_timer); $("#scroll-to-bottom-button-container").addClass("show"); } $(document).on("keydown", (e) => { if (e.shiftKey || e.ctrlKey || e.metaKey) { return; } // Hide scroll to bottom button on any keypress. // Keyboard users are very less likely to use this button. $("#scroll-to-bottom-button-container").removeClass("show"); }); export function is_actively_scrolling() { return actively_scrolling; } export function scroll_finished() { actively_scrolling = false; message_lists.current.view.update_sticky_recipient_headers(); hide_scroll_to_bottom(); if (recent_topics_util.is_visible()) { return; } if (compose_banner.scroll_to_message_banner_message_id !== null) { const $message_row = message_lists.current.get_row( compose_banner.scroll_to_message_banner_message_id, ); if ($message_row.length > 0 && !message_viewport.is_message_below_viewport($message_row)) { compose_banner.clear_message_sent_banners(); } } if (update_selection_on_next_scroll) { message_viewport.keep_pointer_in_view(); } else { update_selection_on_next_scroll = true; } if (message_viewport.at_top()) { message_fetch.maybe_load_older_messages({ msg_list: message_lists.current, }); } if (message_viewport.at_bottom()) { message_fetch.maybe_load_newer_messages({ msg_list: message_lists.current, }); } // When the window scrolls, it may cause some messages to // enter the screen and become read. Calling // unread_ops.process_visible will update necessary // data structures and DOM elements. setTimeout(unread_ops.process_visible, 0); } let scroll_timer; function scroll_finish() { actively_scrolling = true; // Don't present the "scroll to bottom" widget if the current // scroll was triggered by the keyboard. if (!keyboard_triggered_current_scroll) { show_scroll_to_bottom_button(); } keyboard_triggered_current_scroll = false; clearTimeout(scroll_timer); scroll_timer = setTimeout(scroll_finished, 100); } export function initialize() { message_viewport.$message_pane.on( "scroll", _.throttle(() => { unread_ops.process_visible(); scroll_finish(); }, 50), ); // Scroll handler that marks messages as read when you scroll past them. $(document).on("message_selected.zulip", (event) => { if (event.id === -1) { return; } if (event.mark_read && event.previously_selected_id !== -1) { // Mark messages between old pointer and new pointer as read let messages; if (event.id < event.previously_selected_id) { messages = event.msg_list.message_range(event.id, event.previously_selected_id); } else { messages = event.msg_list.message_range(event.previously_selected_id, event.id); } if (event.msg_list.can_mark_messages_read()) { unread_ops.notify_server_messages_read(messages, {from: "pointer"}); } else if ( unread.get_unread_messages(messages).length !== 0 && // The below checks might seem redundant, but it's // possible this logic, which runs after a delay, lost // a race with switching to another view, like Recent // Topics, and we don't want to displ[ay this banner // in such a view. // // This can likely be fixed more cleanly with another approach. narrow_state.filter() !== undefined && message_lists.current === event.msg_list ) { unread_ui.notify_messages_remain_unread(); } } }); }