Keyboard shortcuts and accessibility
This commit is contained in:
parent
8590a047c7
commit
20a892247f
87 changed files with 3652 additions and 711 deletions
|
@ -130,7 +130,7 @@ type ConversationRemovedActionType = {
|
|||
id: string;
|
||||
};
|
||||
};
|
||||
type ConversationUnloadedActionType = {
|
||||
export type ConversationUnloadedActionType = {
|
||||
type: 'CONVERSATION_UNLOADED';
|
||||
payload: {
|
||||
id: string;
|
||||
|
@ -140,6 +140,13 @@ export type RemoveAllConversationsActionType = {
|
|||
type: 'CONVERSATIONS_REMOVE_ALL';
|
||||
payload: null;
|
||||
};
|
||||
export type MessageSelectedActionType = {
|
||||
type: 'MESSAGE_SELECTED';
|
||||
payload: {
|
||||
messageId: string;
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
export type MessageChangedActionType = {
|
||||
type: 'MESSAGE_CHANGED';
|
||||
payload: {
|
||||
|
@ -228,7 +235,7 @@ type ShowInboxActionType = {
|
|||
type: 'SHOW_INBOX';
|
||||
payload: null;
|
||||
};
|
||||
type ShowArchivedConversationsActionType = {
|
||||
export type ShowArchivedConversationsActionType = {
|
||||
type: 'SHOW_ARCHIVED_CONVERSATIONS';
|
||||
payload: null;
|
||||
};
|
||||
|
@ -239,6 +246,7 @@ export type ConversationActionType =
|
|||
| ConversationRemovedActionType
|
||||
| ConversationUnloadedActionType
|
||||
| RemoveAllConversationsActionType
|
||||
| MessageSelectedActionType
|
||||
| MessageChangedActionType
|
||||
| MessageDeletedActionType
|
||||
| MessagesAddedActionType
|
||||
|
@ -264,6 +272,7 @@ export const actions = {
|
|||
conversationRemoved,
|
||||
conversationUnloaded,
|
||||
removeAllConversations,
|
||||
selectMessage,
|
||||
messageDeleted,
|
||||
messageChanged,
|
||||
messagesAdded,
|
||||
|
@ -328,6 +337,16 @@ function removeAllConversations(): RemoveAllConversationsActionType {
|
|||
};
|
||||
}
|
||||
|
||||
function selectMessage(messageId: string, conversationId: string) {
|
||||
return {
|
||||
type: 'MESSAGE_SELECTED',
|
||||
payload: {
|
||||
messageId,
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function messageChanged(
|
||||
id: string,
|
||||
conversationId: string,
|
||||
|
@ -632,9 +651,14 @@ export function reducer(
|
|||
}
|
||||
|
||||
const { messageIds } = existingConversation;
|
||||
const selectedConversation =
|
||||
state.selectedConversation !== id
|
||||
? state.selectedConversation
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedConversation,
|
||||
messagesLookup: omit(state.messagesLookup, messageIds),
|
||||
messagesByConversation: omit(state.messagesByConversation, [id]),
|
||||
};
|
||||
|
@ -642,6 +666,19 @@ export function reducer(
|
|||
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
||||
return getEmptyState();
|
||||
}
|
||||
if (action.type === 'MESSAGE_SELECTED') {
|
||||
const { messageId, conversationId } = action.payload;
|
||||
|
||||
if (state.selectedConversation !== conversationId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedMessage: messageId,
|
||||
selectedMessageCounter: state.selectedMessageCounter + 1,
|
||||
};
|
||||
}
|
||||
if (action.type === 'MESSAGE_CHANGED') {
|
||||
const { id, conversationId, data } = action.payload;
|
||||
const existingConversation = state.messagesByConversation[conversationId];
|
||||
|
@ -712,7 +749,9 @@ export function reducer(
|
|||
[conversationId]: {
|
||||
isLoadingMessages: false,
|
||||
scrollToMessageId,
|
||||
scrollToMessageCounter: 0,
|
||||
scrollToMessageCounter: existingConversation
|
||||
? existingConversation.scrollToMessageCounter + 1
|
||||
: 0,
|
||||
messageIds,
|
||||
metrics,
|
||||
resetCounter,
|
||||
|
|
|
@ -12,10 +12,12 @@ import { makeLookup } from '../../util/makeLookup';
|
|||
|
||||
import {
|
||||
ConversationType,
|
||||
ConversationUnloadedActionType,
|
||||
MessageDeletedActionType,
|
||||
MessageType,
|
||||
RemoveAllConversationsActionType,
|
||||
SelectedConversationChangedActionType,
|
||||
ShowArchivedConversationsActionType,
|
||||
} from './conversations';
|
||||
|
||||
// State
|
||||
|
@ -29,6 +31,7 @@ export type MessageSearchResultLookupType = {
|
|||
};
|
||||
|
||||
export type SearchStateType = {
|
||||
startSearchCounter: number;
|
||||
searchConversationId?: string;
|
||||
searchConversationName?: string;
|
||||
// We store just ids of conversations, since that data is always cached in memory
|
||||
|
@ -81,6 +84,10 @@ type UpdateSearchTermActionType = {
|
|||
query: string;
|
||||
};
|
||||
};
|
||||
type StartSearchActionType = {
|
||||
type: 'SEARCH_START';
|
||||
payload: null;
|
||||
};
|
||||
type ClearSearchActionType = {
|
||||
type: 'SEARCH_CLEAR';
|
||||
payload: null;
|
||||
|
@ -103,18 +110,22 @@ export type SEARCH_TYPES =
|
|||
| SearchMessagesResultsFulfilledActionType
|
||||
| SearchDiscussionsResultsFulfilledActionType
|
||||
| UpdateSearchTermActionType
|
||||
| StartSearchActionType
|
||||
| ClearSearchActionType
|
||||
| ClearConversationSearchActionType
|
||||
| SearchInConversationActionType
|
||||
| MessageDeletedActionType
|
||||
| RemoveAllConversationsActionType
|
||||
| SelectedConversationChangedActionType;
|
||||
| SelectedConversationChangedActionType
|
||||
| ShowArchivedConversationsActionType
|
||||
| ConversationUnloadedActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
searchMessages,
|
||||
searchDiscussions,
|
||||
startSearch,
|
||||
clearSearch,
|
||||
clearConversationSearch,
|
||||
searchInConversation,
|
||||
|
@ -188,7 +199,12 @@ async function doSearchDiscussions(
|
|||
query,
|
||||
};
|
||||
}
|
||||
|
||||
function startSearch(): StartSearchActionType {
|
||||
return {
|
||||
type: 'SEARCH_START',
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
function clearSearch(): ClearSearchActionType {
|
||||
return {
|
||||
type: 'SEARCH_CLEAR',
|
||||
|
@ -294,6 +310,7 @@ async function queryConversationsAndContacts(
|
|||
|
||||
function getEmptyState(): SearchStateType {
|
||||
return {
|
||||
startSearchCounter: 0,
|
||||
query: '',
|
||||
messageIds: [],
|
||||
messageLookup: {},
|
||||
|
@ -304,11 +321,24 @@ function getEmptyState(): SearchStateType {
|
|||
};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
||||
export function reducer(
|
||||
state: SearchStateType = getEmptyState(),
|
||||
action: SEARCH_TYPES
|
||||
): SearchStateType {
|
||||
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
if (action.type === 'SEARCH_START') {
|
||||
return {
|
||||
...state,
|
||||
searchConversationId: undefined,
|
||||
searchConversationName: undefined,
|
||||
startSearchCounter: state.startSearchCounter + 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'SEARCH_CLEAR') {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
@ -341,13 +371,17 @@ export function reducer(
|
|||
const { searchConversationId, searchConversationName } = payload;
|
||||
|
||||
if (searchConversationId === state.searchConversationId) {
|
||||
return state;
|
||||
return {
|
||||
...state,
|
||||
startSearchCounter: state.startSearchCounter + 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...getEmptyState(),
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
startSearchCounter: state.startSearchCounter + 1,
|
||||
};
|
||||
}
|
||||
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
|
||||
|
@ -412,6 +446,18 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'CONVERSATION_UNLOADED') {
|
||||
const { payload } = action;
|
||||
const { id } = payload;
|
||||
const { searchConversationId } = state;
|
||||
|
||||
if (searchConversationId && searchConversationId === id) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
if (action.type === 'MESSAGE_DELETED') {
|
||||
const { messageIds, messageLookup } = state;
|
||||
if (!messageIds || messageIds.length < 1) {
|
||||
|
|
|
@ -8,6 +8,7 @@ export type UserStateType = {
|
|||
stickersPath: string;
|
||||
tempPath: string;
|
||||
ourNumber: string;
|
||||
platform: string;
|
||||
regionCode: string;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
@ -49,6 +50,7 @@ function getEmptyState(): UserStateType {
|
|||
tempPath: 'missing',
|
||||
ourNumber: 'missing',
|
||||
regionCode: 'missing',
|
||||
platform: 'missing',
|
||||
i18n: () => 'missing',
|
||||
};
|
||||
}
|
||||
|
|
16
ts/state/roots/createShortcutGuideModal.tsx
Normal file
16
ts/state/roots/createShortcutGuideModal.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { SmartShortcutGuideModal } from '../smart/ShortcutGuideModal';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredShortcutGuideModal = SmartShortcutGuideModal as any;
|
||||
|
||||
export const createShortcutGuideModal = (store: Store, props: Object) => (
|
||||
<Provider store={store}>
|
||||
<FilteredShortcutGuideModal {...props} />
|
||||
</Provider>
|
||||
);
|
|
@ -50,6 +50,11 @@ export const getSearchConversationName = createSelector(
|
|||
(state: SearchStateType): string | undefined => state.searchConversationName
|
||||
);
|
||||
|
||||
export const getStartSearchCounter = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): number => state.startSearchCounter
|
||||
);
|
||||
|
||||
export const isSearching = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType) => {
|
||||
|
@ -64,12 +69,19 @@ export const getMessageSearchResultLookup = createSelector(
|
|||
(state: SearchStateType) => state.messageLookup
|
||||
);
|
||||
export const getSearchResults = createSelector(
|
||||
[getSearch, getRegionCode, getConversationLookup, getSelectedConversation],
|
||||
[
|
||||
getSearch,
|
||||
getRegionCode,
|
||||
getConversationLookup,
|
||||
getSelectedConversation,
|
||||
getSelectedMessage,
|
||||
],
|
||||
(
|
||||
state: SearchStateType,
|
||||
regionCode: string,
|
||||
lookup: ConversationLookupType,
|
||||
selectedConversation?: string
|
||||
selectedConversationId?: string,
|
||||
selectedMessageId?: string
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
): SearchResultsPropsType | undefined => {
|
||||
const {
|
||||
|
@ -115,7 +127,7 @@ export const getSearchResults = createSelector(
|
|||
type: 'conversation',
|
||||
data: {
|
||||
...data,
|
||||
isSelected: Boolean(data && id === selectedConversation),
|
||||
isSelected: Boolean(data && id === selectedConversationId),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -141,7 +153,7 @@ export const getSearchResults = createSelector(
|
|||
type: 'contact',
|
||||
data: {
|
||||
...data,
|
||||
isSelected: Boolean(data && id === selectedConversation),
|
||||
isSelected: Boolean(data && id === selectedConversationId),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -177,6 +189,8 @@ export const getSearchResults = createSelector(
|
|||
regionCode: regionCode,
|
||||
searchConversationName,
|
||||
searchTerm: state.query,
|
||||
selectedConversationId,
|
||||
selectedMessageId,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -32,6 +32,11 @@ export const getStickersPath = createSelector(
|
|||
(state: UserStateType): string => state.stickersPath
|
||||
);
|
||||
|
||||
export const getPlatform = createSelector(
|
||||
getUser,
|
||||
(state: UserStateType): string => state.platform
|
||||
);
|
||||
|
||||
export const getTempPath = createSelector(
|
||||
getUser,
|
||||
(state: UserStateType): string => state.tempPath
|
||||
|
|
|
@ -6,7 +6,11 @@ import { StateType } from '../reducer';
|
|||
|
||||
import { getSearchResults, isSearching } from '../selectors/search';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
|
||||
import {
|
||||
getLeftPaneLists,
|
||||
getSelectedConversation,
|
||||
getShowArchived,
|
||||
} from '../selectors/conversations';
|
||||
|
||||
import { SmartMainHeader } from './MainHeader';
|
||||
import { SmartMessageSearchResult } from './MessageSearchResult';
|
||||
|
@ -28,10 +32,12 @@ const mapStateToProps = (state: StateType) => {
|
|||
|
||||
const lists = showSearch ? undefined : getLeftPaneLists(state);
|
||||
const searchResults = showSearch ? getSearchResults(state) : undefined;
|
||||
const selectedConversationId = getSelectedConversation(state);
|
||||
|
||||
return {
|
||||
...lists,
|
||||
searchResults,
|
||||
selectedConversationId,
|
||||
showArchived: getShowArchived(state),
|
||||
i18n: getIntl(state),
|
||||
renderMainHeader,
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
getQuery,
|
||||
getSearchConversationId,
|
||||
getSearchConversationName,
|
||||
getStartSearchCounter,
|
||||
} from '../selectors/search';
|
||||
import { getIntl, getRegionCode, getUserNumber } from '../selectors/user';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
|
@ -17,6 +18,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
searchTerm: getQuery(state),
|
||||
searchConversationId: getSearchConversationId(state),
|
||||
searchConversationName: getSearchConversationName(state),
|
||||
startSearchCounter: getStartSearchCounter(state),
|
||||
regionCode: getRegionCode(state),
|
||||
ourNumber: getUserNumber(state),
|
||||
...getMe(state),
|
||||
|
|
47
ts/state/smart/ShortcutGuideModal.tsx
Normal file
47
ts/state/smart/ShortcutGuideModal.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { ShortcutGuideModal } from '../../components/ShortcutGuideModal';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { countStickers } from '../../components/stickers/lib';
|
||||
import { getIntl, getPlatform } from '../selectors/user';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
getKnownStickerPacks,
|
||||
getReceivedStickerPacks,
|
||||
} from '../selectors/stickers';
|
||||
|
||||
type ExternalProps = {
|
||||
close: () => unknown;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { close } = props;
|
||||
|
||||
const blessedPacks = getBlessedStickerPacks(state);
|
||||
const installedPacks = getInstalledStickerPacks(state);
|
||||
const knownPacks = getKnownStickerPacks(state);
|
||||
const receivedPacks = getReceivedStickerPacks(state);
|
||||
|
||||
const hasInstalledStickers =
|
||||
countStickers({
|
||||
knownPacks,
|
||||
blessedPacks,
|
||||
installedPacks,
|
||||
receivedPacks,
|
||||
}) === 0;
|
||||
|
||||
const platform = getPlatform(state);
|
||||
|
||||
return {
|
||||
close,
|
||||
hasInstalledStickers,
|
||||
platform,
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartShortcutGuideModal = smart(ShortcutGuideModal);
|
|
@ -9,6 +9,7 @@ import { getIntl } from '../selectors/user';
|
|||
import {
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
getSelectedMessage,
|
||||
} from '../selectors/conversations';
|
||||
|
||||
import { SmartTimelineItem } from './TimelineItem';
|
||||
|
@ -30,8 +31,18 @@ type ExternalProps = {
|
|||
// are provided by ConversationView in setupTimeline().
|
||||
};
|
||||
|
||||
function renderItem(messageId: string, actionProps: Object): JSX.Element {
|
||||
return <FilteredSmartTimelineItem {...actionProps} id={messageId} />;
|
||||
function renderItem(
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
actionProps: Object
|
||||
): JSX.Element {
|
||||
return (
|
||||
<FilteredSmartTimelineItem
|
||||
{...actionProps}
|
||||
conversationId={conversationId}
|
||||
id={messageId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||
|
@ -48,11 +59,13 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
|
||||
const conversation = getConversationSelector(state)(id);
|
||||
const conversationMessages = getConversationMessagesSelector(state)(id);
|
||||
const selectedMessage = getSelectedMessage(state);
|
||||
|
||||
return {
|
||||
id,
|
||||
...pick(conversation, ['unreadCount', 'typingContact']),
|
||||
...conversationMessages,
|
||||
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
|
||||
i18n: getIntl(state),
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
|
|
|
@ -5,20 +5,30 @@ import { StateType } from '../reducer';
|
|||
|
||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getMessageSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getMessageSelector,
|
||||
getSelectedMessage,
|
||||
} from '../selectors/conversations';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
const { id, conversationId } = props;
|
||||
|
||||
const messageSelector = getMessageSelector(state);
|
||||
const item = messageSelector(id);
|
||||
|
||||
const selectedMessage = getSelectedMessage(state);
|
||||
const isSelected = Boolean(selectedMessage && id === selectedMessage.id);
|
||||
|
||||
return {
|
||||
item,
|
||||
id,
|
||||
conversationId,
|
||||
isSelected,
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue