Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions shared/chat/conversation/attachment-fullscreen/index.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ const Fullscreen = React.memo(function Fullscreen(p: Props) {
}, [fullHeight, fullWidth])

const vidRef = React.useRef<HTMLVideoElement>(null)
const hotKeys = ['left', 'right']
const onHotKey = (cmd: string) => {
cmd === 'left' && onPreviousAttachment()
cmd === 'right' && onNextAttachment()
}
const onHotKey = React.useCallback(
(cmd: string) => {
cmd === 'left' && onPreviousAttachment()
cmd === 'right' && onNextAttachment()
},
[onPreviousAttachment, onNextAttachment]
)
Kb.useHotKey(['left', 'right'], onHotKey)
const isDownloadError = !!message.transferErrMsg

const {showPopup, popup, popupAnchor} = useMessagePopup({ordinal})
Expand All @@ -78,7 +81,6 @@ const Fullscreen = React.memo(function Fullscreen(p: Props) {
return (
<Kb.PopupDialog onClose={onClose} fill={true}>
<Kb.Box style={styles.container}>
<Kb.HotKey hotKeys={hotKeys} onHotKey={onHotKey} />
<Kb.Box style={styles.headerFooter}>
<Kb.Markdown lineClamp={2} style={Kb.Styles.globalStyles.flexOne} styleOverride={titleOverride}>
{title}
Expand Down
3 changes: 1 addition & 2 deletions shared/chat/conversation/normal/index.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const LoadingLine = () => {
return showLoader ? <Kb.LoadingLine /> : null
}

const hotKeys = ['mod+f']
const Conversation = React.memo(function Conversation() {
const conversationIDKey = Chat.useChatContext(s => s.id)
const navigateAppend = Chat.useChatNavigateAppend()
Expand Down Expand Up @@ -69,10 +68,10 @@ const Conversation = React.memo(function Conversation() {
const onToggleThreadSearch = React.useCallback(() => {
toggleThreadSearch()
}, [toggleThreadSearch])
Kb.useHotKey('mod+f', onToggleThreadSearch)

return (
<div className="conversation" style={styles.container} onPaste={onPaste} key={conversationIDKey}>
<Kb.HotKey hotKeys={hotKeys} onHotKey={onToggleThreadSearch} />
<Kb.DragAndDrop
onAttach={cannotWrite ? undefined : onAttach}
fullHeight={true}
Expand Down
16 changes: 9 additions & 7 deletions shared/chat/conversation/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,15 @@ const ThreadSearchDesktop = React.memo(function ThreadSearchDesktop(p: OwnProps)
const {conversationIDKey, submitSearch, hits, selectResult, onEnter} = props
const {onUp, onDown, onChangedText, inProgress, hasResults} = props
const {selectedIndex, status, text, style, onToggleThreadSearch} = props
const hotKeys = ['esc']
const onHotKey = (cmd: string) => {
if (cmd === 'esc') {
onToggleThreadSearch()
}
}
const onHotKey = React.useCallback(
(cmd: string) => {
if (cmd === 'esc') {
onToggleThreadSearch()
}
},
[onToggleThreadSearch]
)
Kb.useHotKey('esc', onHotKey)
const inputRef = React.createRef<Kb.PlainInputRef>()
const onKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
Expand Down Expand Up @@ -206,7 +209,6 @@ const ThreadSearchDesktop = React.memo(function ThreadSearchDesktop(p: OwnProps)
const noResults = status === 'done' && hits.length === 0
return (
<Kb.Box2 direction="vertical" fullWidth={true} style={style}>
<Kb.HotKey hotKeys={hotKeys} onHotKey={onHotKey} />
<Kb.Box2 direction="horizontal" style={styles.outerContainer} fullWidth={true} gap="tiny">
<Kb.Box2 direction="horizontal" style={styles.inputContainer}>
<Kb.Box2 direction="horizontal" gap="xtiny" style={styles.queryContainer} centerChildren={true}>
Expand Down
3 changes: 1 addition & 2 deletions shared/chat/inbox/filter-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ type OwnProps = {
showSearch: boolean
}

const hotKeys = ['mod+n']

const ConversationFilterInput = React.memo(function ConversationFilterInput(ownProps: OwnProps) {
const {onEnsureSelection, onSelectDown, onSelectUp, showSearch} = ownProps
Expand Down Expand Up @@ -75,6 +74,7 @@ const ConversationFilterInput = React.memo(function ConversationFilterInput(ownP
const onHotKeys = React.useCallback(() => {
appendNewChatBuilder()
}, [appendNewChatBuilder])
Kb.useHotKey('mod+n', onHotKeys)

React.useEffect(() => {
if (isSearching) {
Expand Down Expand Up @@ -118,7 +118,6 @@ const ConversationFilterInput = React.memo(function ConversationFilterInput(ownP
gapStart={showSearch}
gapEnd={showSearch}
>
{!Kb.Styles.isMobile && <Kb.HotKey hotKeys={hotKeys} onHotKey={onHotKeys} />}
{showSearch && searchInput}
</Kb.Box2>
)
Expand Down
8 changes: 1 addition & 7 deletions shared/common-adapters/hot-key.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
import type * as React from 'react'
type Props = {
hotKeys: Array<string> | string
onHotKey: (key: string) => void
}
declare const HotKey: (p: Props) => React.ReactNode
declare function useHotKey(keys: Array<string> | string, cb: (key: string) => void): void
export function useHotKey(keys: Array<string> | string, cb: (key: string) => void): void
214 changes: 165 additions & 49 deletions shared/common-adapters/hot-key.desktop.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,193 @@
import * as React from 'react'
import * as C from '@/constants'
import Mousetrap from 'mousetrap'
import {registerDebugClear} from '@/util/debug'
import {isMac} from '@/constants/platform'

registerDebugClear(() => {
Mousetrap.reset()
})
const keyToCBStack = new Map<string, Array<(cmd: string) => void>>()

// mousetrap is very simple. a bind will overwrite the binding. unbind unbinds it globally
// we need to keep a stack to manage the state. We register/unregister when we mount/unmount / change nav focus
const normalizeKey = (key: string): string => {
const lower = key.toLowerCase()
if (lower === 'mod') {
return isMac ? 'meta' : 'ctrl'
}
if (lower === 'cmd' || lower === 'command') {
return 'meta'
}
return lower
}

const keyToCBStack = new Map<string, Array<(cmd: string) => void>>()
const parseKeyCombo = (
combo: string
): {
key: string
ctrl: boolean
shift: boolean
alt: boolean
meta: boolean
hasMod: boolean
} => {
const parts = combo.split('+').map(p => p.trim().toLowerCase())
let key = ''
let ctrl = false
let shift = false
let alt = false
let meta = false
let hasMod = false

for (const part of parts) {
const lower = part.toLowerCase()
if (lower === 'mod') {
hasMod = true
if (isMac) {
meta = true
} else {
ctrl = true
}
} else {
const normalized = normalizeKey(part)
if (normalized === 'ctrl') {
ctrl = true
} else if (normalized === 'shift') {
shift = true
} else if (normalized === 'alt') {
alt = true
} else if (normalized === 'meta') {
meta = true
} else {
key = part
}
}
}

return {alt, ctrl, hasMod, key, meta, shift}
}

const matchesCombo = (
e: KeyboardEvent,
combo: {key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean; hasMod: boolean}
): boolean => {
const eventKey = e.key.toLowerCase()
const comboKey = combo.key.toLowerCase()

const keyMatches =
eventKey === comboKey ||
(comboKey === 'esc' && eventKey === 'escape') ||
(comboKey === 'left' && eventKey === 'arrowleft') ||
(comboKey === 'right' && eventKey === 'arrowright') ||
(comboKey === 'up' && eventKey === 'arrowup') ||
(comboKey === 'down' && eventKey === 'arrowdown') ||
(comboKey === 'space' && eventKey === ' ') ||
(comboKey === 'enter' && eventKey === 'enter') ||
(comboKey === 'tab' && eventKey === 'tab') ||
(comboKey === 'backspace' && eventKey === 'backspace') ||
(comboKey === 'delete' && eventKey === 'delete')

if (!keyMatches) {
return false
}

if (combo.hasMod) {
if (isMac) {
return e.metaKey && !e.ctrlKey && e.shiftKey === combo.shift && e.altKey === combo.alt
} else {
return e.ctrlKey && !e.metaKey && e.shiftKey === combo.shift && e.altKey === combo.alt
}
}

return (
e.ctrlKey === combo.ctrl &&
e.shiftKey === combo.shift &&
e.altKey === combo.alt &&
e.metaKey === combo.meta
)
}

const shouldIgnoreEvent = (e: KeyboardEvent): boolean => {
const target = e.target as HTMLElement | null
if (!target) {
return false
}

if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
return target.getAttribute('data-allow-keyboard-shortcuts') !== 'true'
}

return false
}

const handleKeyDown = (e: KeyboardEvent): void => {
if (shouldIgnoreEvent(e)) {
return
}

for (const [comboStr, callbacks] of keyToCBStack.entries()) {
if (callbacks.length === 0) {
continue
}
const combo = parseKeyCombo(comboStr)
if (matchesCombo(e, combo)) {
const lastCallback = callbacks[callbacks.length - 1]
if (lastCallback) {
e.preventDefault()
e.stopPropagation()
lastCallback(comboStr)
}
break
}
}
}

let globalHandlerAttached = false

const ensureGlobalHandler = (): void => {
if (!globalHandlerAttached) {
globalHandlerAttached = true
document.addEventListener('keydown', handleKeyDown, true)
}
}

const removeGlobalHandler = (): void => {
if (globalHandlerAttached && keyToCBStack.size === 0) {
globalHandlerAttached = false
document.removeEventListener('keydown', handleKeyDown, true)
}
}

/** hook for hotkeys **/
export function useHotKey(keys: Array<string> | string, cb: (key: string) => void) {
const keysArr = React.useMemo(() => (typeof keys === 'string' ? [keys] : keys), [keys])
const keysArr = React.useMemo(() => {
const arr = typeof keys === 'string' ? [keys] : keys
return arr.filter(k => k.length > 0)
}, [keys])

const register = React.useCallback(() => {
// add key and callback to bookkeeping
if (keysArr.length === 0) {
return
}
ensureGlobalHandler()
keysArr.forEach(key => {
let cbs = keyToCBStack.get(key)
const normalizedKey = key.toLowerCase().trim()
let cbs = keyToCBStack.get(normalizedKey)
if (!cbs) {
cbs = []
keyToCBStack.set(key, cbs)
keyToCBStack.set(normalizedKey, cbs)
}
cbs.push(cb)
})
// actually bind
Mousetrap.bind(
keysArr,
(e: {stopPropagation: () => void}, key: string) => {
e.stopPropagation()
key && cb(key)
},
'keydown'
)
}, [keysArr, cb])

const unregister = React.useCallback(() => {
// find and remove the bookkeeping
keysArr.forEach(key => {
const cbs = keyToCBStack.get(key)
const normalizedKey = key.toLowerCase().trim()
const cbs = keyToCBStack.get(normalizedKey)
if (!cbs) return
const idx = cbs.indexOf(cb)
if (idx !== -1) {
cbs.splice(idx, 1)
// mousetrap will remove existing bindings. if there is an older one turn it back on
const last = cbs.at(-1)
if (last) {
Mousetrap.bind(
key,
(e: {stopPropagation: () => void}, key: string) => {
e.stopPropagation()
key && last(key)
},
'keydown'
)
}
}
// nothing listening for this key? now we finally unbind and cleanup
if (cbs.length === 0) {
Mousetrap.unbind(key, 'keydown')
keyToCBStack.delete(key)
keyToCBStack.delete(normalizedKey)
}
})
removeGlobalHandler()
}, [keysArr, cb])

C.Router2.useSafeFocusEffect(
Expand All @@ -79,14 +204,5 @@ export function useHotKey(keys: Array<string> | string, cb: (key: string) => voi
return () => {
unregister()
}
}, [keys, cb, register, unregister])
}, [register, unregister])
}

/** Simple component to control a global key binding **/
export const HotKey = React.memo(function HotKey(p: {
hotKeys: Array<string> | string
onHotKey: (key: string) => void
}) {
useHotKey(p.hotKeys, p.onHotKey)
return null
})
4 changes: 2 additions & 2 deletions shared/common-adapters/hot-key.native.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export function useHotKey() {}
export const HotKey = () => null
export function useHotKey(_keys: Array<string> | string, _cb: (key: string) => void) {}

3 changes: 0 additions & 3 deletions shared/common-adapters/index-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,6 @@ module.exports = {
get HeaderLeftCancel() {
return require('./header-hoc').HeaderLeftCancel
},
get HotKey() {
return require('./hot-key').HotKey
},
get Icon() {
return require('./icon').default
},
Expand Down
2 changes: 1 addition & 1 deletion shared/common-adapters/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export {default as FloatingPicker} from './floating-picker'
export {usePopup2, type Popup2Parms} from './use-popup'
export {HeaderHocHeader, HeaderHocWrapper, HeaderLeftBlank, HeaderLeftCancel} from './header-hoc'
export {PopupWrapper} from './header-or-popup'
export {HotKey, useHotKey} from './hot-key'
export {useHotKey} from './hot-key'
export {default as Icon, urlsToImgSet, type IconStyle} from './icon'
export {default as Image2} from './image2'
export {default as InfoNote} from './info-note'
Expand Down
Loading