Modernize forward message modal

* ModalHost is replaced with Modal.
 * The CSS is simplified down to unify the theme palette
This commit is contained in:
hackerbirds 2023-06-15 22:52:29 +02:00
parent 91399deb26
commit 4ff36f46c4
3 changed files with 135 additions and 269 deletions

View file

@ -2,27 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
.module-ForwardMessageModal { .module-ForwardMessageModal {
$padding: 16px;
@include popper-shadow();
border-radius: 8px;
display: flex;
flex-direction: column; flex-direction: column;
margin-block: 0; margin-block: 0;
margin-inline: auto; margin-inline: auto;
max-height: 90vh;
max-width: 360px; max-width: 360px;
width: 95%; width: 95%;
@include light-theme() {
background: $color-white;
color: $color-gray-90;
}
@include dark-theme() {
background: $color-gray-75;
color: $color-gray-05;
}
.module-conversation-list { .module-conversation-list {
// remove horizontal padding so ListTiles extend to the edges // remove horizontal padding so ListTiles extend to the edges
padding: 0; padding: 0;
@ -43,6 +28,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
position: relative; position: relative;
@include font-body-1-bold;
&--edit { &--edit {
border-bottom: 1px solid $color-gray-15; border-bottom: 1px solid $color-gray-15;
@ -51,73 +37,6 @@
border-color: $color-gray-60; border-color: $color-gray-60;
} }
} }
&--close {
@include button-reset;
position: absolute;
inset-inline-end: 16px;
height: 20px;
width: 20px;
@include light-theme {
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-75);
}
@include dark-theme {
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-15);
}
@include keyboard-mode {
&:focus {
background-color: $color-ultramarine;
}
}
}
&--back {
@include button-reset;
height: 24px;
inset-inline-start: 16px;
position: absolute;
width: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-left.svg',
$color-gray-60
);
}
@include keyboard-mode {
&:focus {
@include color-svg(
'../images/icons/v3/chevron/chevron-left.svg',
$color-ultramarine
);
}
}
@include dark-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-left.svg',
$color-gray-25
);
}
@include dark-keyboard-mode {
&:hover {
@include color-svg(
'../images/icons/v3/chevron/chevron-left.svg',
$color-ultramarine-light
);
}
}
}
h1 {
@include font-body-1-bold;
}
} }
&__list-wrapper { &__list-wrapper {
@ -176,26 +95,18 @@
} }
} }
// Disable vertical scrolling on the modal pages
// since the elements inside are scrollable themselves
.module-Modal__body {
overflow-y: hidden;
}
&__footer { &__footer {
@include font-body-2; @include font-body-2;
align-items: center;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
display: flex; display: flex;
align-items: center;
flex-grow: 1;
justify-content: space-between; justify-content: space-between;
margin-top: 0;
padding: $padding;
position: relative;
@include light-theme {
background-color: $color-gray-02;
color: $color-gray-60;
}
@include dark-theme() {
background: $color-gray-65;
color: $color-gray-25;
}
} }
// Disable cursor since images are non-clickable // Disable cursor since images are non-clickable

View file

@ -41,6 +41,7 @@
&__back-button { &__back-button {
@include button-reset; @include button-reset;
border-radius: 4px; border-radius: 4px;
height: 20px; height: 20px;
width: 20px; width: 20px;
@ -68,16 +69,7 @@
&:hover, &:hover,
&:focus { &:focus {
background: $color-gray-45; box-shadow: 0 0 0 2px $color-ultramarine;
&::before {
background-color: $color-ultramarine;
}
}
&:active {
background: $color-gray-45;
&::before {
background-color: $color-ultramarine;
}
} }
} }

View file

@ -11,9 +11,6 @@ import React, {
} from 'react'; } from 'react';
import type { MeasuredComponentProps } from 'react-measure'; import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { animated } from '@react-spring/web';
import classNames from 'classnames';
import { AttachmentList } from './conversation/AttachmentList'; import { AttachmentList } from './conversation/AttachmentList';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { Button } from './Button'; import { Button } from './Button';
@ -25,11 +22,9 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { LocalizerType, ThemeType } from '../types/Util'; import type { LocalizerType, ThemeType } from '../types/Util';
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
import { ModalHost } from './ModalHost';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { useAnimated } from '../hooks/useAnimated';
import { import {
shouldNeverBeCalled, shouldNeverBeCalled,
asyncShouldNeverBeCalled, asyncShouldNeverBeCalled,
@ -46,6 +41,7 @@ import type { ShowToastAction } from '../state/ducks/toast';
import type { HydratedBodyRangesType } from '../types/BodyRange'; import type { HydratedBodyRangesType } from '../types/BodyRange';
import { BodyRange } from '../types/BodyRange'; import { BodyRange } from '../types/BodyRange';
import { UserText } from './UserText'; import { UserText } from './UserText';
import { Modal } from './Modal';
export type DataPropsType = { export type DataPropsType = {
candidateConversations: ReadonlyArray<ConversationType>; candidateConversations: ReadonlyArray<ConversationType>;
@ -212,24 +208,13 @@ export function ForwardMessagesModal({
[contactLookup, selectedContacts, setSelectedContacts] [contactLookup, selectedContacts, setSelectedContacts]
); );
const { close, modalStyles, overlayStyles } = useAnimated(onClose, {
getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }),
getTo: isOpen =>
isOpen
? { opacity: 1, transform: 'translateY(0px)' }
: {
opacity: 0,
transform: 'translateY(48px)',
},
});
const handleBackOrClose = useCallback(() => { const handleBackOrClose = useCallback(() => {
if (isEditingMessage) { if (isEditingMessage) {
setIsEditingMessage(false); setIsEditingMessage(false);
} else { } else {
close(); onClose();
} }
}, [isEditingMessage, close, setIsEditingMessage]); }, [isEditingMessage, onClose, setIsEditingMessage]);
const rowCount = filteredConversations.length; const rowCount = filteredConversations.length;
const getRow = (index: number): undefined | Row => { const getRow = (index: number): undefined | Row => {
@ -263,6 +248,38 @@ export function ForwardMessagesModal({
}; };
}, []); }, []);
const footer = (
<div className="module-ForwardMessageModal__footer">
<div>
{selectedContacts.map((contact, index) => {
return (
<Fragment key={contact.id}>
<UserText text={contact.title} />
{index < selectedContacts.length - 1 ? ', ' : ''}
</Fragment>
);
})}
</div>
<div>
{isEditingMessage || !isLonelyDraftEditable ? (
<Button
aria-label={i18n('icu:ForwardMessageModal--continue')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
aria-disabled={!canForwardMessages}
onClick={forwardMessages}
/>
) : (
<Button
aria-label={i18n('icu:forwardMessage')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
disabled={!hasContactsSelected}
onClick={() => setIsEditingMessage(true)}
/>
)}
</div>
</div>
);
return ( return (
<> <>
{cannotMessage && ( {cannotMessage && (
@ -275,155 +292,101 @@ export function ForwardMessagesModal({
{i18n('icu:GroupV2--cannot-send')} {i18n('icu:GroupV2--cannot-send')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
<ModalHost <Modal
modalName="ForwardMessageModal" modalName="ForwardMessageModal"
noMouseClose hasXButton
onEscape={handleBackOrClose} i18n={i18n}
onClose={close} onClose={onClose}
overlayStyles={overlayStyles} onBackButtonClick={isEditingMessage ? handleBackOrClose : undefined}
moduleClassName="module-ForwardMessageModal"
title={i18n('icu:ForwardMessageModal__title')}
useFocusTrap={false} useFocusTrap={false}
padded={false}
modalFooter={footer}
> >
<animated.div {isEditingMessage && lonelyDraft != null ? (
className="module-ForwardMessageModal" <ForwardMessageEditor
style={modalStyles} draft={lonelyDraft}
> linkPreview={lonelyLinkPreview}
<div onChange={(messageBody, bodyRanges) => {
className={classNames('module-ForwardMessageModal__header', { onChange([{ ...lonelyDraft, messageBody, bodyRanges }]);
'module-ForwardMessageModal__header--edit': isEditingMessage, }}
})} onChangeAttachments={attachments => {
> onChange([{ ...lonelyDraft, attachments }]);
{isEditingMessage ? ( }}
<button removeLinkPreview={removeLinkPreview}
aria-label={i18n('icu:back')} theme={theme}
className="module-ForwardMessageModal__header--back" i18n={i18n}
onClick={() => setIsEditingMessage(false)} RenderCompositionTextArea={RenderCompositionTextArea}
type="button" onSubmit={forwardMessages}
> />
&nbsp; ) : (
</button> <div className="module-ForwardMessageModal__main-body">
) : ( <SearchInput
<button disabled={candidateConversations.length === 0}
aria-label={i18n('icu:close')}
className="module-ForwardMessageModal__header--close"
onClick={close}
type="button"
/>
)}
<h1>{i18n('icu:ForwardMessageModal__title')}</h1>
</div>
{isEditingMessage && lonelyDraft != null ? (
<ForwardMessageEditor
draft={lonelyDraft}
linkPreview={lonelyLinkPreview}
onChange={(messageBody, bodyRanges) => {
onChange([{ ...lonelyDraft, messageBody, bodyRanges }]);
}}
onChangeAttachments={attachments => {
onChange([{ ...lonelyDraft, attachments }]);
}}
removeLinkPreview={removeLinkPreview}
theme={theme}
i18n={i18n} i18n={i18n}
RenderCompositionTextArea={RenderCompositionTextArea} placeholder={i18n('icu:contactSearchPlaceholder')}
onSubmit={forwardMessages} onChange={event => {
setSearchTerm(event.target.value);
}}
ref={inputRef}
value={searchTerm}
/> />
) : ( {candidateConversations.length ? (
<div className="module-ForwardMessageModal__main-body"> <Measure bounds>
<SearchInput {({ contentRect, measureRef }: MeasuredComponentProps) => (
disabled={candidateConversations.length === 0} <div
i18n={i18n} className="module-ForwardMessageModal__list-wrapper"
placeholder={i18n('icu:contactSearchPlaceholder')} ref={measureRef}
onChange={event => { >
setSearchTerm(event.target.value); <ConversationList
}} dimensions={contentRect.bounds}
ref={inputRef} getPreferredBadge={getPreferredBadge}
value={searchTerm} getRow={getRow}
/> i18n={i18n}
{candidateConversations.length ? ( onClickArchiveButton={shouldNeverBeCalled}
<Measure bounds> onClickContactCheckbox={(
{({ contentRect, measureRef }: MeasuredComponentProps) => ( conversationId: string,
<div disabledReason:
className="module-ForwardMessageModal__list-wrapper" | undefined
ref={measureRef} | ContactCheckboxDisabledReason
> ) => {
<ConversationList if (
dimensions={contentRect.bounds} disabledReason !==
getPreferredBadge={getPreferredBadge} ContactCheckboxDisabledReason.MaximumContactsSelected
getRow={getRow} ) {
i18n={i18n} toggleSelectedConversation(conversationId);
onClickArchiveButton={shouldNeverBeCalled} }
onClickContactCheckbox={( }}
conversationId: string, lookupConversationWithoutUuid={asyncShouldNeverBeCalled}
disabledReason: showConversation={shouldNeverBeCalled}
| undefined showUserNotFoundModal={shouldNeverBeCalled}
| ContactCheckboxDisabledReason setIsFetchingUUID={shouldNeverBeCalled}
) => { onSelectConversation={shouldNeverBeCalled}
if ( blockConversation={shouldNeverBeCalled}
disabledReason !== removeConversation={shouldNeverBeCalled}
ContactCheckboxDisabledReason.MaximumContactsSelected onOutgoingAudioCallInConversation={shouldNeverBeCalled}
) { onOutgoingVideoCallInConversation={shouldNeverBeCalled}
toggleSelectedConversation(conversationId); renderMessageSearchResult={() => {
} shouldNeverBeCalled();
}} return <div />;
lookupConversationWithoutUuid={asyncShouldNeverBeCalled} }}
showConversation={shouldNeverBeCalled} rowCount={rowCount}
showUserNotFoundModal={shouldNeverBeCalled} shouldRecomputeRowHeights={false}
setIsFetchingUUID={shouldNeverBeCalled} showChooseGroupMembers={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled} theme={theme}
blockConversation={shouldNeverBeCalled} />
removeConversation={shouldNeverBeCalled} </div>
onOutgoingAudioCallInConversation={shouldNeverBeCalled} )}
onOutgoingVideoCallInConversation={shouldNeverBeCalled} </Measure>
renderMessageSearchResult={() => { ) : (
shouldNeverBeCalled(); <div className="module-ForwardMessageModal__no-candidate-contacts">
return <div />; {i18n('icu:noContactsFound')}
}} </div>
rowCount={rowCount} )}
shouldRecomputeRowHeights={false}
showChooseGroupMembers={shouldNeverBeCalled}
theme={theme}
/>
</div>
)}
</Measure>
) : (
<div className="module-ForwardMessageModal__no-candidate-contacts">
{i18n('icu:noContactsFound')}
</div>
)}
</div>
)}
<div className="module-ForwardMessageModal__footer">
<div>
{selectedContacts.map((contact, index) => {
return (
<Fragment key={contact.id}>
<UserText text={contact.title} />
{index < selectedContacts.length - 1 ? ', ' : ''}
</Fragment>
);
})}
</div>
<div>
{isEditingMessage || !isLonelyDraftEditable ? (
<Button
aria-label={i18n('icu:ForwardMessageModal--continue')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
aria-disabled={!canForwardMessages}
onClick={forwardMessages}
/>
) : (
<Button
aria-label={i18n('icu:forwardMessage')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
disabled={!hasContactsSelected}
onClick={() => setIsEditingMessage(true)}
/>
)}
</div>
</div> </div>
</animated.div> )}
</ModalHost> </Modal>
</> </>
); );
} }