Disable search keyboard shortcuts when main header isn't shown

This commit is contained in:
Evan Hahn 2021-02-12 15:58:14 -06:00 committed by Josh Perez
parent 011bdd2ae3
commit eb203ba929
10 changed files with 166 additions and 70 deletions

View file

@ -948,10 +948,9 @@ type WhatIsThis = import('./window.d').WhatIsThis;
const commandKey = window.platform === 'darwin' && metaKey; const commandKey = window.platform === 'darwin' && metaKey;
const controlKey = window.platform !== 'darwin' && ctrlKey; const controlKey = window.platform !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey; const commandOrCtrl = commandKey || controlKey;
const commandAndCtrl = commandKey && ctrlKey;
const state = store.getState(); const state = store.getState();
const selectedId = state.conversations.selectedConversation; const selectedId = state.conversations.selectedConversationId;
const conversation = window.ConversationController.get(selectedId); const conversation = window.ConversationController.get(selectedId);
const isSearching = window.Signal.State.Selectors.search.isSearching( const isSearching = window.Signal.State.Selectors.search.isSearching(
state state
@ -1265,40 +1264,6 @@ type WhatIsThis = import('./window.d').WhatIsThis;
return; return;
} }
// Search
if (
commandOrCtrl &&
!commandAndCtrl &&
!shiftKey &&
(key === 'f' || key === 'F')
) {
const { startSearch } = actions.search;
startSearch();
event.preventDefault();
event.stopPropagation();
return;
}
// Search in conversation
if (
conversation &&
commandOrCtrl &&
!commandAndCtrl &&
shiftKey &&
(key === 'f' || key === 'F')
) {
const { searchInConversation } = actions.search;
const name = conversation.isMe()
? window.i18n('noteToSelf')
: conversation.getTitle();
searchInConversation(conversation.id, name);
event.preventDefault();
event.stopPropagation();
return;
}
// Focus composer field // Focus composer field
if ( if (
conversation && conversation &&

View file

@ -33,6 +33,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
'searchConversationId', 'searchConversationId',
overrideProps.searchConversationId overrideProps.searchConversationId
), ),
selectedConversation: undefined,
startSearchCounter: 0, startSearchCounter: 0,
ourConversationId: '', ourConversationId: '',
@ -46,10 +47,12 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
avatarPath: optionalText('avatarPath', overrideProps.avatarPath), avatarPath: optionalText('avatarPath', overrideProps.avatarPath),
i18n, i18n,
updateSearchTerm: action('updateSearchTerm'), updateSearchTerm: action('updateSearchTerm'),
searchMessages: action('searchMessages'), searchMessages: action('searchMessages'),
searchDiscussions: action('searchDiscussions'), searchDiscussions: action('searchDiscussions'),
startSearch: action('startSearch'),
searchInConversation: action('searchInConversation'),
clearConversationSearch: action('clearConversationSearch'), clearConversationSearch: action('clearConversationSearch'),
clearSearch: action('clearSearch'), clearSearch: action('clearSearch'),

View file

@ -12,12 +12,14 @@ import { Avatar } from './Avatar';
import { AvatarPopup } from './AvatarPopup'; import { AvatarPopup } from './AvatarPopup';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { ColorType } from '../types/Colors'; import { ColorType } from '../types/Colors';
import { ConversationType } from '../state/ducks/conversations';
export type PropsType = { export type PropsType = {
searchTerm: string; searchTerm: string;
searchConversationName?: string; searchConversationName?: string;
searchConversationId?: string; searchConversationId?: string;
startSearchCounter: number; startSearchCounter: number;
selectedConversation: undefined | ConversationType;
// To be used as an ID // To be used as an ID
ourConversationId: string; ourConversationId: string;
@ -36,7 +38,10 @@ export type PropsType = {
avatarPath?: string; avatarPath?: string;
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => void; updateSearchTerm: (searchTerm: string) => void;
startSearch: () => void;
searchInConversation: (id: string, name: string) => void;
searchMessages: ( searchMessages: (
query: string, query: string,
options: { options: {
@ -53,7 +58,6 @@ export type PropsType = {
noteToSelf: string; noteToSelf: string;
} }
) => void; ) => void;
clearConversationSearch: () => void; clearConversationSearch: () => void;
clearSearch: () => void; clearSearch: () => void;
@ -107,12 +111,6 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
}; };
public handleOutsideKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
this.hideAvatarPopup();
}
};
public showAvatarPopup = (): void => { public showAvatarPopup = (): void => {
const popperRoot = document.createElement('div'); const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot); document.body.appendChild(popperRoot);
@ -122,14 +120,12 @@ export class MainHeader extends React.Component<PropsType, StateType> {
popperRoot, popperRoot,
}); });
document.addEventListener('click', this.handleOutsideClick); document.addEventListener('click', this.handleOutsideClick);
document.addEventListener('keydown', this.handleOutsideKeyDown);
}; };
public hideAvatarPopup = (): void => { public hideAvatarPopup = (): void => {
const { popperRoot } = this.state; const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick); document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleOutsideKeyDown);
this.setState({ this.setState({
showingAvatarPopup: false, showingAvatarPopup: false,
@ -141,11 +137,15 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
}; };
public componentDidMount(): void {
document.addEventListener('keydown', this.handleGlobalKeyDown);
}
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { popperRoot } = this.state; const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick); document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleOutsideKeyDown); document.removeEventListener('keydown', this.handleGlobalKeyDown);
if (popperRoot && document.body.contains(popperRoot)) { if (popperRoot && document.body.contains(popperRoot)) {
document.body.removeChild(popperRoot); document.body.removeChild(popperRoot);
@ -225,7 +225,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
this.setFocus(); this.setFocus();
}; };
public handleKeyDown = ( public handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement> event: React.KeyboardEvent<HTMLInputElement>
): void => { ): void => {
const { const {
@ -262,6 +262,50 @@ export class MainHeader extends React.Component<PropsType, StateType> {
event.stopPropagation(); event.stopPropagation();
}; };
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
const { showingAvatarPopup } = this.state;
const {
i18n,
selectedConversation,
startSearch,
searchInConversation,
} = this.props;
const { ctrlKey, metaKey, shiftKey, key } = event;
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey;
const commandAndCtrl = commandKey && ctrlKey;
if (showingAvatarPopup && key === 'Escape') {
this.hideAvatarPopup();
} else if (
commandOrCtrl &&
!commandAndCtrl &&
!shiftKey &&
(key === 'f' || key === 'F')
) {
startSearch();
event.preventDefault();
event.stopPropagation();
} else if (
selectedConversation &&
commandOrCtrl &&
!commandAndCtrl &&
shiftKey &&
(key === 'f' || key === 'F')
) {
const name = selectedConversation.isMe
? i18n('noteToSelf')
: selectedConversation.title;
searchInConversation(selectedConversation.id, name);
event.preventDefault();
event.stopPropagation();
}
};
public handleXButton = (): void => { public handleXButton = (): void => {
const { const {
searchConversationId, searchConversationId,
@ -398,7 +442,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
)} )}
placeholder={placeholder} placeholder={placeholder}
dir="auto" dir="auto"
onKeyDown={this.handleKeyDown} onKeyDown={this.handleInputKeyDown}
value={searchTerm} value={searchTerm}
onChange={this.updateSearch} onChange={this.updateSearch}
/> />

View file

@ -225,7 +225,7 @@ export type ConversationsStateType = {
conversationsByE164: ConversationLookupType; conversationsByE164: ConversationLookupType;
conversationsByUuid: ConversationLookupType; conversationsByUuid: ConversationLookupType;
conversationsByGroupId: ConversationLookupType; conversationsByGroupId: ConversationLookupType;
selectedConversation?: string; selectedConversationId?: string;
selectedMessage?: string; selectedMessage?: string;
selectedMessageCounter: number; selectedMessageCounter: number;
selectedConversationTitle?: string; selectedConversationTitle?: string;
@ -981,7 +981,7 @@ export function reducer(
const { id, data } = payload; const { id, data } = payload;
const { conversationLookup } = state; const { conversationLookup } = state;
let { showArchived, selectedConversation } = state; let { showArchived, selectedConversationId } = state;
const existing = conversationLookup[id]; const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation // In the change case we only modify the lookup if we already had that conversation
@ -989,7 +989,7 @@ export function reducer(
return state; return state;
} }
if (selectedConversation === id) { if (selectedConversationId === id) {
// Archived -> Inbox: we go back to the normal inbox view // Archived -> Inbox: we go back to the normal inbox view
if (existing.isArchived && !data.isArchived) { if (existing.isArchived && !data.isArchived) {
showArchived = false; showArchived = false;
@ -999,13 +999,13 @@ export function reducer(
// behavior - no selected conversation in the left pane, but a conversation show // behavior - no selected conversation in the left pane, but a conversation show
// in the right pane. // in the right pane.
if (!existing.isArchived && data.isArchived) { if (!existing.isArchived && data.isArchived) {
selectedConversation = undefined; selectedConversationId = undefined;
} }
} }
return { return {
...state, ...state,
selectedConversation, selectedConversationId,
showArchived, showArchived,
conversationLookup: { conversationLookup: {
...conversationLookup, ...conversationLookup,
@ -1040,14 +1040,14 @@ export function reducer(
} }
const { messageIds } = existingConversation; const { messageIds } = existingConversation;
const selectedConversation = const selectedConversationId =
state.selectedConversation !== id state.selectedConversationId !== id
? state.selectedConversation ? state.selectedConversationId
: undefined; : undefined;
return { return {
...state, ...state,
selectedConversation, selectedConversationId,
selectedConversationPanelDepth: 0, selectedConversationPanelDepth: 0,
messagesLookup: omit(state.messagesLookup, messageIds), messagesLookup: omit(state.messagesLookup, messageIds),
messagesByConversation: omit(state.messagesByConversation, [id]), messagesByConversation: omit(state.messagesByConversation, [id]),
@ -1065,7 +1065,7 @@ export function reducer(
if (action.type === 'MESSAGE_SELECTED') { if (action.type === 'MESSAGE_SELECTED') {
const { messageId, conversationId } = action.payload; const { messageId, conversationId } = action.payload;
if (state.selectedConversation !== conversationId) { if (state.selectedConversationId !== conversationId) {
return state; return state;
} }
@ -1621,7 +1621,7 @@ export function reducer(
return { return {
...state, ...state,
selectedConversation: id, selectedConversationId: id,
}; };
} }
if (action.type === 'SHOW_INBOX') { if (action.type === 'SHOW_INBOX') {

View file

@ -22,6 +22,7 @@ import { getCallsByConversation } from './calling';
import { getBubbleProps } from '../../shims/Whisper'; import { getBubbleProps } from '../../shims/Whisper';
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline'; import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import { TimelineItemType } from '../../components/conversation/TimelineItem'; import { TimelineItemType } from '../../components/conversation/TimelineItem';
import { assert } from '../../util/assert';
import { import {
getInteractionMode, getInteractionMode,
@ -83,10 +84,29 @@ export const getConversationsByGroupId = createSelector(
} }
); );
export const getSelectedConversation = createSelector( export const getSelectedConversationId = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): string | undefined => { (state: ConversationsStateType): string | undefined => {
return state.selectedConversation; return state.selectedConversationId;
}
);
export const getSelectedConversation = createSelector(
getSelectedConversationId,
getConversationLookup,
(
selectedConversationId: string | undefined,
conversationLookup: ConversationLookupType
): undefined | ConversationType => {
if (!selectedConversationId) {
return undefined;
}
const conversation = getOwn(conversationLookup, selectedConversationId);
assert(
conversation,
'getSelectedConversation: could not find selected conversation in lookup; returning undefined'
);
return conversation;
} }
); );
@ -221,7 +241,7 @@ export const _getLeftPaneLists = (
export const getLeftPaneLists = createSelector( export const getLeftPaneLists = createSelector(
getConversationLookup, getConversationLookup,
getConversationComparator, getConversationComparator,
getSelectedConversation, getSelectedConversationId,
getPinnedConversationIds, getPinnedConversationIds,
_getLeftPaneLists _getLeftPaneLists
); );

View file

@ -29,7 +29,7 @@ import {
GetConversationByIdType, GetConversationByIdType,
getConversationLookup, getConversationLookup,
getConversationSelector, getConversationSelector,
getSelectedConversation, getSelectedConversationId,
} from './conversations'; } from './conversations';
export const getSearch = (state: StateType): SearchStateType => state.search; export const getSearch = (state: StateType): SearchStateType => state.search;
@ -78,7 +78,7 @@ export const getSearchResults = createSelector(
getRegionCode, getRegionCode,
getUserAgent, getUserAgent,
getConversationLookup, getConversationLookup,
getSelectedConversation, getSelectedConversationId,
getSelectedMessage, getSelectedMessage,
], ],
( (

View file

@ -11,7 +11,7 @@ import { getSearchResults, isSearching } from '../selectors/search';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { import {
getLeftPaneLists, getLeftPaneLists,
getSelectedConversation, getSelectedConversationId,
getShowArchived, getShowArchived,
} from '../selectors/conversations'; } from '../selectors/conversations';
@ -52,7 +52,7 @@ const mapStateToProps = (state: StateType) => {
const lists = showSearch ? undefined : getLeftPaneLists(state); const lists = showSearch ? undefined : getLeftPaneLists(state);
const searchResults = showSearch ? getSearchResults(state) : undefined; const searchResults = showSearch ? getSearchResults(state) : undefined;
const selectedConversationId = getSelectedConversation(state); const selectedConversationId = getSelectedConversationId(state);
return { return {
...lists, ...lists,

View file

@ -20,13 +20,14 @@ import {
getUserNumber, getUserNumber,
getUserUuid, getUserUuid,
} from '../selectors/user'; } from '../selectors/user';
import { getMe } from '../selectors/conversations'; import { getMe, getSelectedConversation } from '../selectors/conversations';
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
return { return {
searchTerm: getQuery(state), searchTerm: getQuery(state),
searchConversationId: getSearchConversationId(state), searchConversationId: getSearchConversationId(state),
searchConversationName: getSearchConversationName(state), searchConversationName: getSearchConversationName(state),
selectedConversation: getSelectedConversation(state),
startSearchCounter: getStartSearchCounter(state), startSearchCounter: getStartSearchCounter(state),
regionCode: getRegionCode(state), regionCode: getRegionCode(state),
ourConversationId: getUserConversationId(state), ourConversationId: getUserConversationId(state),

View file

@ -13,6 +13,8 @@ import {
_getLeftPaneLists, _getLeftPaneLists,
getConversationSelector, getConversationSelector,
getPlaceholderContact, getPlaceholderContact,
getSelectedConversation,
getSelectedConversationId,
} from '../../../state/selectors/conversations'; } from '../../../state/selectors/conversations';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import { StateType, reducer as rootReducer } from '../../../state/reducer'; import { StateType, reducer as rootReducer } from '../../../state/reducer';
@ -446,4 +448,65 @@ describe('both/state/selectors/conversations', () => {
}); });
}); });
}); });
describe('#getSelectedConversationId', () => {
it('returns undefined if no conversation is selected', () => {
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
abc123: getDefaultConversation('abc123'),
},
},
};
assert.isUndefined(getSelectedConversationId(state));
});
it('returns the selected conversation ID', () => {
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
abc123: getDefaultConversation('abc123'),
},
selectedConversationId: 'abc123',
},
};
assert.strictEqual(getSelectedConversationId(state), 'abc123');
});
});
describe('#getSelectedConversation', () => {
it('returns undefined if no conversation is selected', () => {
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
abc123: getDefaultConversation('abc123'),
},
},
};
assert.isUndefined(getSelectedConversation(state));
});
it('returns the selected conversation ID', () => {
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
abc123: getDefaultConversation('abc123'),
},
selectedConversationId: 'abc123',
},
};
assert.deepEqual(
getSelectedConversation(state),
getDefaultConversation('abc123')
);
});
});
}); });

View file

@ -14631,7 +14631,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.js", "path": "ts/components/MainHeader.js",
"line": " this.inputRef = react_1.default.createRef();", "line": " this.inputRef = react_1.default.createRef();",
"lineNumber": 146, "lineNumber": 171,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
@ -14640,7 +14640,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.tsx", "path": "ts/components/MainHeader.tsx",
"line": " this.inputRef = React.createRef();", "line": " this.inputRef = React.createRef();",
"lineNumber": 74, "lineNumber": 78,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"