import $ from "jquery"; import * as activity from "./activity"; import * as activity_ui from "./activity_ui"; import * as browser_history from "./browser_history"; import * as common from "./common"; import * as compose from "./compose"; import * as compose_actions from "./compose_actions"; import * as compose_banner from "./compose_banner"; import * as compose_recipient from "./compose_recipient"; import * as compose_reply from "./compose_reply"; import * as compose_state from "./compose_state"; import * as compose_textarea from "./compose_textarea"; import * as condense from "./condense"; import * as copy_and_paste from "./copy_and_paste"; import * as deprecated_feature_notice from "./deprecated_feature_notice"; import * as drafts_overlay_ui from "./drafts_overlay_ui"; import * as emoji from "./emoji"; import * as emoji_picker from "./emoji_picker"; import * as feedback_widget from "./feedback_widget"; import * as gear_menu from "./gear_menu"; import * as giphy from "./giphy"; import * as hash_util from "./hash_util"; import * as hashchange from "./hashchange"; import * as hotspots from "./hotspots"; import * as inbox_ui from "./inbox_ui"; import * as lightbox from "./lightbox"; import * as list_util from "./list_util"; import * as message_actions_popover from "./message_actions_popover"; import * as message_edit from "./message_edit"; import * as message_edit_history from "./message_edit_history"; import * as message_lists from "./message_lists"; import * as message_scroll_state from "./message_scroll_state"; import * as modals from "./modals"; import * as narrow from "./narrow"; import * as narrow_state from "./narrow_state"; import * as navbar_menus from "./navbar_menus"; import * as navigate from "./navigate"; import * as overlays from "./overlays"; import {page_params} from "./page_params"; import * as playground_links_popover from "./playground_links_popover"; import * as popover_menus from "./popover_menus"; import * as popovers from "./popovers"; import * as reactions from "./reactions"; import * as recent_view_ui from "./recent_view_ui"; import * as recent_view_util from "./recent_view_util"; import * as scheduled_messages_overlay_ui from "./scheduled_messages_overlay_ui"; import * as scheduled_messages_popover from "./scheduled_messages_popover"; import * as search from "./search"; import * as settings_data from "./settings_data"; import * as sidebar_ui from "./sidebar_ui"; import * as spectators from "./spectators"; import * as starred_messages_ui from "./starred_messages_ui"; import * as stream_data from "./stream_data"; import * as stream_list from "./stream_list"; import * as stream_popover from "./stream_popover"; import * as stream_settings_ui from "./stream_settings_ui"; import * as topic_list from "./topic_list"; import * as unread_ops from "./unread_ops"; import * as user_card_popover from "./user_card_popover"; import * as user_group_popover from "./user_group_popover"; import {user_settings} from "./user_settings"; import * as user_topics_ui from "./user_topics_ui"; function do_narrow_action(action) { action(message_lists.current.selected_id(), {trigger: "hotkey"}); return true; } // For message actions and user profile menu. const menu_dropdown_hotkeys = new Set(["down_arrow", "up_arrow", "vim_up", "vim_down", "enter"]); // Note that multiple keys can map to the same event_name, which // we'll do in cases where they have the exact same semantics. // DON'T FORGET: update keyboard_shortcuts.html // The `message_view_only` property is a convenient and performant way // to express a common case of which hotkeys do something in which // views. It is set for hotkeys (like `Ctrl + S`) that only have an effect // in the main message view with a selected message. // `message_view_only` hotkeys, as a group, are not processed if any // overlays are open (e.g. settings, streams, etc.). const keydown_shift_mappings = { // these can be triggered by Shift + key only 9: {name: "shift_tab", message_view_only: false}, // Tab 32: {name: "shift_spacebar", message_view_only: true}, // space bar 37: {name: "left_arrow", message_view_only: false}, // left arrow 39: {name: "right_arrow", message_view_only: false}, // right arrow 38: {name: "up_arrow", message_view_only: false}, // up arrow 40: {name: "down_arrow", message_view_only: false}, // down arrow 72: {name: "view_edit_history", message_view_only: true}, // 'H' 78: {name: "narrow_to_next_unread_followed_topic", message_view_only: false}, // 'N' }; const keydown_unshift_mappings = { // these can be triggered by key only (without Shift) 9: {name: "tab", message_view_only: false}, // Tab 27: {name: "escape", message_view_only: false}, // Esc 32: {name: "spacebar", message_view_only: true}, // space bar 33: {name: "page_up", message_view_only: true}, // PgUp 34: {name: "page_down", message_view_only: true}, // PgDn 35: {name: "end", message_view_only: true}, // End 36: {name: "home", message_view_only: true}, // Home 37: {name: "left_arrow", message_view_only: false}, // left arrow 39: {name: "right_arrow", message_view_only: false}, // right arrow 38: {name: "up_arrow", message_view_only: false}, // up arrow 40: {name: "down_arrow", message_view_only: false}, // down arrow }; const keydown_ctrl_mappings = { 219: {name: "escape", message_view_only: false}, // '[' 13: {name: "ctrl_enter", message_view_only: true}, // enter }; const keydown_cmd_or_ctrl_mappings = { 67: {name: "copy_with_c", message_view_only: false}, // 'C' 75: {name: "search_with_k", message_view_only: false}, // 'K' 83: {name: "star_message", message_view_only: true}, // 'S' 190: {name: "narrow_to_compose_target", message_view_only: true}, // '.' }; const keydown_either_mappings = { // these can be triggered by key or Shift + key // Note that codes for letters are still case sensitive! // // We may want to revisit both of these. For Backspace, we don't // have any specific mapping behavior; we are just trying to disable // the normal browser features for certain OSes when we are in the // compose box, and the little bit of Backspace-related code here is // dubious, but may apply to Shift-Backspace. // For Enter, there is some possibly that Shift-Enter is intended to // have special behavior for folks that are used to Shift-Enter behavior // in other apps, but that's also slightly dubious. 8: {name: "backspace", message_view_only: true}, // Backspace 13: {name: "enter", message_view_only: false}, // Enter 46: {name: "delete", message_view_only: false}, // Delete }; const keypress_mappings = { 42: {name: "star_deprecated", message_view_only: true}, // '*' 43: {name: "thumbs_up_emoji", message_view_only: true}, // '+' 61: {name: "upvote_first_emoji", message_view_only: true}, // '=' 45: {name: "toggle_message_collapse", message_view_only: true}, // '-' 47: {name: "search", message_view_only: false}, // '/' 58: {name: "toggle_reactions_popover", message_view_only: true}, // ':' 62: {name: "compose_quote_reply", message_view_only: true}, // '>' 63: {name: "show_shortcuts", message_view_only: false}, // '?' 64: {name: "compose_reply_with_mention", message_view_only: true}, // '@' 65: {name: "stream_cycle_backward", message_view_only: true}, // 'A' 67: {name: "C_deprecated", message_view_only: true}, // 'C' 68: {name: "stream_cycle_forward", message_view_only: true}, // 'D' 71: {name: "G_end", message_view_only: true}, // 'G' 73: {name: "open_inbox", message_view_only: true}, // 'I' 74: {name: "vim_page_down", message_view_only: true}, // 'J' 75: {name: "vim_page_up", message_view_only: true}, // 'K' 77: {name: "toggle_topic_visibility_policy", message_view_only: true}, // 'M' 80: {name: "narrow_private", message_view_only: true}, // 'P' 82: {name: "respond_to_author", message_view_only: true}, // 'R' 83: {name: "toggle_stream_subscription", message_view_only: true}, // 'S' 85: {name: "mark_unread", message_view_only: true}, // 'U' 86: {name: "view_selected_stream", message_view_only: false}, // 'V' 97: {name: "all_messages", message_view_only: true}, // 'a' 99: {name: "compose", message_view_only: true}, // 'c' 100: {name: "open_drafts", message_view_only: true}, // 'd' 101: {name: "edit_message", message_view_only: true}, // 'e' 103: {name: "gear_menu", message_view_only: true}, // 'g' 104: {name: "vim_left", message_view_only: true}, // 'h' 105: {name: "message_actions", message_view_only: true}, // 'i' 106: {name: "vim_down", message_view_only: true}, // 'j' 107: {name: "vim_up", message_view_only: true}, // 'k' 108: {name: "vim_right", message_view_only: true}, // 'l' 109: {name: "move_message", message_view_only: true}, // 'm' 110: {name: "n_key", message_view_only: false}, // 'n' 112: {name: "p_key", message_view_only: false}, // 'p' 113: {name: "query_streams", message_view_only: true}, // 'q' 114: {name: "reply_message", message_view_only: true}, // 'r' 115: {name: "toggle_conversation_view", message_view_only: true}, // 's' 116: {name: "open_recent_view", message_view_only: true}, // 't' 117: {name: "toggle_sender_info", message_view_only: true}, // 'u' 118: {name: "show_lightbox", message_view_only: true}, // 'v' 119: {name: "query_users", message_view_only: true}, // 'w' 120: {name: "compose_private_message", message_view_only: true}, // 'x' 122: {name: "zoom_to_message_near", message_view_only: true}, // 'z' }; export function get_keydown_hotkey(e) { if (e.altKey) { return undefined; } let hotkey; if (e.ctrlKey && !e.shiftKey) { hotkey = keydown_ctrl_mappings[e.which]; if (hotkey) { return hotkey; } } const isCmdOrCtrl = common.has_mac_keyboard() ? e.metaKey : e.ctrlKey; if (isCmdOrCtrl && !e.shiftKey) { hotkey = keydown_cmd_or_ctrl_mappings[e.which]; if (hotkey) { return hotkey; } return undefined; } else if (e.metaKey || e.ctrlKey) { return undefined; } if (e.shiftKey) { hotkey = keydown_shift_mappings[e.which]; if (hotkey) { return hotkey; } } if (!e.shiftKey) { hotkey = keydown_unshift_mappings[e.which]; if (hotkey) { return hotkey; } } return keydown_either_mappings[e.which]; } export function get_keypress_hotkey(e) { if (e.metaKey || e.ctrlKey || e.altKey) { return undefined; } return keypress_mappings[e.which]; } export function processing_text() { const $focused_elt = $(":focus"); return ( $focused_elt.is("input") || $focused_elt.is("select") || $focused_elt.is("textarea") || $focused_elt.parents(".pill-container").length >= 1 || $focused_elt.attr("id") === "compose-send-button" || $focused_elt.parents(".dropdown-list-container").length >= 1 ); } export function in_content_editable_widget(e) { return $(e.target).is(".editable-section"); } // Returns true if we handled it, false if the browser should. export function process_escape_key(e) { if ( recent_view_ui.is_in_focus() && // This will return false if `e.target` is not // any of the Recent Conversations elements by design. recent_view_ui.change_focused_element($(e.target), "escape") ) { // Recent Conversations uses escape to switch focus from // search / filters to the conversations table. If focus is // already on the table, it returns false. return true; } if (inbox_ui.is_in_focus() && inbox_ui.change_focused_element("escape")) { return true; } if (feedback_widget.is_open()) { feedback_widget.dismiss(); return true; } if (popovers.any_active()) { if (user_card_popover.manage_menu.is_open()) { user_card_popover.manage_menu.hide(); $("#user_card_popover .user-card-popover-manage-menu-btn").trigger("focus"); return true; } popovers.hide_all(); return true; } if (modals.any_active()) { modals.close_active(); return true; } if (overlays.any_active()) { overlays.close_active(); return true; } if (processing_text()) { if (activity_ui.searching()) { activity_ui.escape_search(); return true; } if (stream_list.searching()) { stream_list.clear_and_hide_search(); return true; } // Emoji picker goes before compose so compose emoji picker is closed properly. if (emoji_picker.is_open()) { emoji_picker.hide_emoji_popover(); return true; } if (giphy.is_popped_from_edit_message()) { giphy.focus_current_edit_message(); // Hide after setting focus so that `edit_message_id` is // still set in giphy. giphy.hide_giphy_popover(); return true; } if (compose_state.composing()) { // Check if the giphy popover was open using compose box. // Hide GIPHY popover if it's open. if (!giphy.is_popped_from_edit_message() && giphy.hide_giphy_popover()) { $("textarea#compose-textarea").trigger("focus"); return true; } // Check for errors in compose box; close errors if they exist if ($("main-view-banner").length) { compose_banner.clear_all(); return true; } // If the user hit the Esc key, cancel the current compose compose_actions.cancel(); return true; } // When the input is focused, we blur and clear the input. A second "Esc" // will zoom out, handled below. if (stream_list.is_zoomed_in() && $("#filter-topic-input").is(":focus")) { topic_list.clear_topic_search(e); return true; } // We pressed Esc and something was focused, and the composebox // wasn't open. In that case, we should blur the input. $("input:focus,textarea:focus").trigger("blur"); return true; } if (sidebar_ui.any_sidebar_expanded_as_overlay()) { sidebar_ui.hide_all(); return true; } if (compose_state.composing()) { compose_actions.cancel(); return true; } if (stream_list.is_zoomed_in()) { stream_list.zoom_out(); return true; } /* The Ctrl+[ hotkey navigates to the home view * unconditionally; Esc's behavior depends on a setting. */ if (user_settings.web_escape_navigates_to_home_view || e.which === 219) { hashchange.set_hash_to_home_view(); return true; } return false; } function handle_popover_events(event_name) { const popover_menu_visible_instance = popover_menus.get_visible_instance(); if (popover_menu_visible_instance) { popover_menus.sidebar_menu_instance_handle_keyboard( popover_menu_visible_instance, event_name, ); return true; } if (user_card_popover.manage_menu.is_open()) { user_card_popover.manage_menu.handle_keyboard(event_name); return true; } if (user_card_popover.message_user_card.is_open()) { user_card_popover.message_user_card.handle_keyboard(event_name); return true; } if (user_card_popover.user_card.is_open()) { user_card_popover.user_card.handle_keyboard(event_name); return true; } if (user_card_popover.user_sidebar.is_open()) { user_card_popover.user_sidebar.handle_keyboard(event_name); return true; } if (stream_popover.is_open()) { stream_popover.stream_sidebar_menu_handle_keyboard(event_name); return true; } if (user_group_popover.is_open()) { user_group_popover.handle_keyboard(event_name); return true; } if (playground_links_popover.is_open()) { playground_links_popover.handle_keyboard(event_name); return true; } return false; } // Returns true if we handled it, false if the browser should. export function process_enter_key(e) { if (popovers.any_active() && $(e.target).hasClass("navigate-link-on-enter")) { // If a popover is open and we pressed Enter on a menu item, // call click directly on the item to navigate to the `href`. // trigger("click") doesn't work for them to navigate to `href`. e.target.click(); e.preventDefault(); popovers.hide_all(); return true; } if (hotspots.is_open()) { $(e.target).find(".hotspot.overlay.show .hotspot-confirm").trigger("click"); return false; } if (emoji_picker.is_open()) { return emoji_picker.navigate("enter", e); } if (handle_popover_events("enter")) { return true; } if (overlays.settings_open()) { // On the settings page just let the browser handle // the Enter key for things like submitting forms. return false; } if (overlays.streams_open()) { return false; } if (processing_text()) { if (stream_list.searching()) { // This is sort of funny behavior, but I think // the intention is that we want it super easy // to close stream search. stream_list.clear_and_hide_search(); return true; } return false; } // This handles when pressing Enter while looking at drafts. // It restores draft that is focused. if (overlays.drafts_open()) { drafts_overlay_ui.handle_keyboard_events("enter"); return true; } if (overlays.scheduled_messages_open()) { scheduled_messages_overlay_ui.handle_keyboard_events("enter"); return true; } // Transfer the enter keypress from button to the `` tag inside // it since it is the trigger for the popover.