Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyboard shortcuts to modals #2977

Merged
merged 10 commits into from
Mar 28, 2024
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"emoji-mart": "^5.5.2",
"filesize": "3.6.1",
"firstline": "1.2.1",
"focus-trap-react": "^10.2.3",
"fs-extra": "9.0.0",
"glob": "7.1.2",
"image-type": "^4.1.0",
Expand Down
117 changes: 61 additions & 56 deletions ts/components/SessionWrapperModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useRef } from 'react';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import React, { useRef } from 'react';
import useKey from 'react-use/lib/useKey';

import { SessionIconButton } from './icon';
Expand Down Expand Up @@ -63,68 +64,72 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
};

return (
<div
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
<FocusTrap focusTrapOptions={{ initialFocus: '#focus-trap-start' }}>
<div
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
>
{/* FocusTrap needs a button always mounted as a start, which is apparently not our case */}
<button id="focus-trap-start" style={{ opacity: 0, width: 0, height: 0 }} />
<div className="session-confirm-wrapper">
yougotwill marked this conversation as resolved.
Show resolved Hide resolved
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
) : null}
) : null}

<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}

<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</FocusTrap>
);
};
8 changes: 6 additions & 2 deletions ts/components/basic/SessionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import styled from 'styled-components';

export enum SessionButtonType {
Expand Down Expand Up @@ -28,7 +28,7 @@ export enum SessionButtonColor {
None = 'transparent',
}

const StyledButton = styled.div<{
const StyledButton = styled.button<{
color: string | undefined;
Bilb marked this conversation as resolved.
Show resolved Hide resolved
buttonType: SessionButtonType;
buttonShape: SessionButtonShape;
Expand Down Expand Up @@ -67,6 +67,10 @@ const StyledButton = styled.div<{
'box-shadow: 0px 0px 6px var(--button-solid-shadow-color);'}
border-radius: ${props => (props.buttonShape === SessionButtonShape.Round ? '17px' : '6px')};

:focus-within {
outline: 1px var(--primary-color) dashed;
}
yougotwill marked this conversation as resolved.
Show resolved Hide resolved

.session-icon {
fill: var(--background-primary-color);
}
Expand Down
4 changes: 4 additions & 0 deletions ts/components/basic/SessionRadio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const StyledInput = styled.input<{
? props.selectedColor
: 'var(--primary-color)'};
}

:focus-within + label:before {
outline: 1px var(--primary-color) dashed;
}
`;

// NOTE (Will): We don't use a transition because it's too slow and creates flickering when changing buttons.
Expand Down
10 changes: 9 additions & 1 deletion ts/components/conversation/SessionConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ import { LightboxGallery, MediaItemType } from '../lightbox/LightboxGallery';
import { NoMessageInConversation } from './SubtleNotification';
import { ConversationHeaderWithDetails } from './header/ConversationHeader';

import { deleteMessagesForX } from '../../interactions/conversations/unsendingInteractions';
import { isAudio } from '../../types/MIME';
import { HTMLDirection } from '../../util/i18n';
import { NoticeBanner } from '../NoticeBanner';
import { SessionSpinner } from '../basic/SessionSpinner';
import { RightPanel, StyledRightPanelContainer } from './right-panel/RightPanel';

const DEFAULT_JPEG_QUALITY = 0.85;

interface State {
isDraggingFile: boolean;
}
Expand All @@ -85,6 +85,7 @@ interface Props {

stagedAttachments: Array<StagedAttachmentType>;
isSelectedConvoInitialLoadingInProgress: boolean;
isPublic: boolean;
}

const StyledSpinnerContainer = styled.div`
Expand Down Expand Up @@ -345,6 +346,7 @@ export class SessionConversation extends React.Component<Props, State> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private onKeyDown(event: any) {
const selectionMode = !!this.props.selectedMessages.length;
const { selectedConversationKey, selectedMessages, isPublic } = this.props;

if (event.target.classList.contains('conversation-content')) {
switch (event.key) {
Expand All @@ -353,6 +355,12 @@ export class SessionConversation extends React.Component<Props, State> {
window.inboxStore?.dispatch(resetSelectedMessageIds());
}
break;
case 'Backspace':
case 'Delete':
if (selectionMode && this.props.selectedConversationKey) {
void deleteMessagesForX(selectedMessages, selectedConversationKey, isPublic);
}
break;
default:
}
}
Expand Down
4 changes: 2 additions & 2 deletions ts/components/conversation/composition/CompositionButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import { SessionIconButton } from '../../icon';
import { Noop } from '../../../types/Util';
import { SessionIconButton } from '../../icon';

const StyledChatButtonContainer = styled.div`
.session-icon-button {
Expand Down Expand Up @@ -50,7 +50,7 @@ export const StartRecordingButton = (props: { onClick: Noop }) => {
};

// eslint-disable-next-line react/display-name
export const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: Noop }>(
export const ToggleEmojiButton = React.forwardRef<HTMLButtonElement, { onClick: Noop }>(
yougotwill marked this conversation as resolved.
Show resolved Hide resolved
(props, ref) => {
return (
<StyledChatButtonContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useKey } from 'react-use';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
deleteMessagesForX,
} from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
import { getSelectedMessageIds } from '../../../state/selectors/conversations';
Expand Down Expand Up @@ -33,6 +35,25 @@ export const SelectionOverlay = () => {
const isPublic = useSelectedIsPublic();
const dispatch = useDispatch();

useKey('Delete', event => {
Bilb marked this conversation as resolved.
Show resolved Hide resolved
const selectionMode = !!selectedMessageIds.length;

switch (event.key) {
case 'Escape':
if (selectionMode) {
dispatch(resetSelectedMessageIds());
}
break;
case 'Backspace':
case 'Delete':
if (selectionMode && selectedConversationKey) {
void deleteMessagesForX(selectedMessageIds, selectedConversationKey, isPublic);
}
break;
default:
}
});

function onCloseOverlay() {
dispatch(resetSelectedMessageIds());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { Data } from '../../../../data/data';

import { MessageInteraction } from '../../../../interactions';
import { replyToMessage } from '../../../../interactions/conversationInteractions';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../../../interactions/conversations/unsendingInteractions';
import { deleteMessagesForX } from '../../../../interactions/conversations/unsendingInteractions';
import {
addSenderAsModerator,
removeSenderFromModerator,
Expand Down Expand Up @@ -97,14 +94,9 @@ const DeleteItem = ({ messageId }: { messageId: string }) => {

const onDelete = useCallback(() => {
if (convoId) {
if (!isPublic && isDeletable) {
void deleteMessagesById([messageId], convoId);
}
if (isPublic && isDeletableForEveryone) {
void deleteMessagesByIdForEveryone([messageId], convoId);
}
void deleteMessagesForX([messageId], convoId, isPublic);
}
}, [convoId, isDeletable, isDeletableForEveryone, isPublic, messageId]);
}, [convoId, isPublic, messageId]);

if (!convoId || (isPublic && !isDeletableForEveryone) || (!isPublic && !isDeletable)) {
return null;
Expand Down
2 changes: 2 additions & 0 deletions ts/components/conversation/right-panel/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ export const StyledRightPanelContainer = styled.div`

background-color: var(--background-primary-color);
border-left: 1px solid var(--border-color);
visibility: hidden;

&.show {
transform: none;
transition: transform 0.3s ease-in-out;
z-index: 3;
visibility: visible;
}
`;

Expand Down
9 changes: 9 additions & 0 deletions ts/components/dialog/SessionConfirm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { shell } from 'electron';
import React, { Dispatch, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { useLastMessage } from '../../hooks/useParamSelector';
import { MessageInteraction } from '../../interactions';
Expand Down Expand Up @@ -119,6 +120,14 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
}
};

useKey('Enter', () => {
void onClickOkHandler();
});

useKey('Escape', () => {
onClickCancelHandler();
});

useEffect(() => {
if (isLoading) {
if (conversationId && lastMessage?.interactionType) {
Expand Down
Loading
Loading