Streamlined system messages

This commit is contained in:
Josh Perez 2021-08-26 16:51:55 -04:00 committed by GitHub
parent 1973224adb
commit 2b08cbfdfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 864 additions and 937 deletions

View file

@ -11,20 +11,21 @@ const story = storiesOf('Components/Button', module);
story.add('Kitchen sink', () => (
<>
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
<React.Fragment key={size}>
{[
ButtonVariant.Primary,
ButtonVariant.Secondary,
ButtonVariant.SecondaryAffirmative,
ButtonVariant.SecondaryDestructive,
ButtonVariant.Destructive,
ButtonVariant.Calling,
].map(variant => (
<React.Fragment key={variant}>
{[
ButtonVariant.Primary,
ButtonVariant.Secondary,
ButtonVariant.SecondaryAffirmative,
ButtonVariant.SecondaryDestructive,
ButtonVariant.Destructive,
ButtonVariant.Calling,
ButtonVariant.SystemMessage,
].map(variant => (
<React.Fragment key={variant}>
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
<React.Fragment key={size}>
<p>
<Button onClick={action('onClick')} size={size} variant={variant}>
Hello world
{variant}
</Button>
</p>
<p>
@ -34,7 +35,7 @@ story.add('Kitchen sink', () => (
size={size}
variant={variant}
>
Hello world
{variant}
</Button>
</p>
</React.Fragment>

View file

@ -12,12 +12,13 @@ export enum ButtonSize {
}
export enum ButtonVariant {
Primary,
Secondary,
SecondaryAffirmative,
SecondaryDestructive,
Destructive,
Calling,
Primary = 'Primary',
Secondary = 'Secondary',
SecondaryAffirmative = 'SecondaryAffirmative',
SecondaryDestructive = 'SecondaryDestructive',
Destructive = 'Destructive',
Calling = 'Calling',
SystemMessage = 'SystemMessage',
}
type PropsType = {
@ -68,6 +69,7 @@ const VARIANT_CLASS_NAMES = new Map<ButtonVariant, string>([
],
[ButtonVariant.Destructive, 'module-Button--destructive'],
[ButtonVariant.Calling, 'module-Button--calling'],
[ButtonVariant.SystemMessage, 'module-Button--system-message'],
]);
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(

View file

@ -2,13 +2,17 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import Measure from 'react-measure';
import { noop } from 'lodash';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { Timestamp } from './Timestamp';
import { LocalizerType } from '../../types/Util';
import { CallMode } from '../../types/Calling';
import {
CallingNotificationType,
getCallingIcon,
getCallingNotificationText,
} from '../../util/callingNotification';
import { usePrevious } from '../../util/hooks';
@ -48,16 +52,18 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
}
}, [height, previousHeight, conversationId, messageId, messageSizeChanged]);
let hasButton = false;
let timestamp: number;
let callType: 'audio' | 'video';
let wasMissed = false;
switch (props.callMode) {
case CallMode.Direct:
timestamp = props.acceptedTime || props.endedTime;
callType = props.wasVideoCall ? 'video' : 'audio';
wasMissed =
props.wasIncoming && !props.acceptedTime && !props.wasDeclined;
break;
case CallMode.Group:
hasButton = !props.ended;
timestamp = props.startedTime;
callType = 'video';
break;
default:
window.log.error(
@ -66,6 +72,8 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
return null;
}
const icon = getCallingIcon(props);
return (
<Measure
bounds
@ -79,24 +87,33 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
>
{({ measureRef }) => (
<div
className={`module-message-calling--notification module-message-calling--${callType}`}
className={classNames('SystemMessage', 'SystemMessage--multiline', {
'SystemMessage--error': wasMissed,
})}
ref={measureRef}
>
<div className={`module-message-calling--${callType}__icon`} />
{getCallingNotificationText(props, i18n)}
<div>
<Timestamp
i18n={i18n}
timestamp={timestamp}
extended
direction="outgoing"
withImageNoCaption={false}
withSticker={false}
withTapToViewExpired={false}
module="module-message__metadata__date"
<div className="SystemMessage__line">
<div
className={`SystemMessage__icon SystemMessage__icon--${icon}`}
/>
<div>
{getCallingNotificationText(props, i18n)} &middot;{' '}
<Timestamp
direction="outgoing"
extended
i18n={i18n}
timestamp={timestamp}
withImageNoCaption={false}
withSticker={false}
withTapToViewExpired={false}
/>
</div>
</div>
<CallingNotificationButton {...props} />
{hasButton ? (
<div className="SystemMessage__line">
<CallingNotificationButton {...props} />
</div>
) : null}
</div>
)}
</Measure>
@ -120,7 +137,7 @@ function CallingNotificationButton(props: PropsType) {
let buttonText: string;
let disabledTooltipText: undefined | string;
let onClick: undefined | (() => void);
let onClick: () => void;
if (activeCallConversationId) {
if (activeCallConversationId === conversationId) {
buttonText = i18n('calling__return');
@ -130,6 +147,7 @@ function CallingNotificationButton(props: PropsType) {
disabledTooltipText = i18n(
'calling__call-notification__button__in-another-call-tooltip'
);
onClick = noop;
}
} else if (deviceCount >= maxDevices) {
buttonText = i18n('calling__call-is-full');
@ -137,6 +155,7 @@ function CallingNotificationButton(props: PropsType) {
'calling__call-notification__button__call-full-tooltip',
[String(deviceCount)]
);
onClick = noop;
} else {
buttonText = i18n('calling__join');
onClick = () => {
@ -145,14 +164,14 @@ function CallingNotificationButton(props: PropsType) {
}
const button = (
<button
className="module-message-calling--notification__button"
<Button
disabled={Boolean(disabledTooltipText)}
onClick={onClick}
type="button"
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{buttonText}
</button>
</Button>
);
if (disabledTooltipText) {

View file

@ -21,14 +21,12 @@ export type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
const CSS_MODULE = 'module-change-number-notification';
export const ChangeNumberNotification: React.FC<Props> = props => {
const { i18n, sender, timestamp } = props;
return (
<div className={CSS_MODULE}>
<span className={`${CSS_MODULE}__icon`} />
<div className="SystemMessage">
<span className="SystemMessage__icon SystemMessage__icon--phone" />
<Intl
id="ChangeNumber--notification"
components={{

View file

@ -5,6 +5,7 @@ import React, { useCallback, useState, ReactElement } from 'react';
import { LocalizerType } from '../../types/Util';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
type PropsHousekeepingType = {
@ -36,18 +37,20 @@ export function ChatSessionRefreshedNotification(
}, [contactSupport, setIsDialogOpen]);
return (
<div className="module-chat-session-refreshed-notification">
<div className="module-chat-session-refreshed-notification__first-line">
<span className="module-chat-session-refreshed-notification__icon" />
<div className="SystemMessage SystemMessage--multiline">
<div className="SystemMessage__line">
<span className="SystemMessage__icon SystemMessage__icon--session-refresh" />
{i18n('ChatRefresh--notification')}
</div>
<button
type="button"
onClick={openDialog}
className="module-chat-session-refreshed-notification__button"
>
{i18n('ChatRefresh--learnMore')}
</button>
<div className="SystemMessage__line">
<Button
onClick={openDialog}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('ChatRefresh--learnMore')}
</Button>
</div>
{isDialogOpen ? (
<ChatSessionRefreshedDialog
onClose={closeDialog}

View file

@ -3,6 +3,7 @@
import React, { useCallback, useState, ReactElement } from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util';
import { Intl } from '../Intl';
@ -45,9 +46,9 @@ export function DeliveryIssueNotification(
}
return (
<div className="module-delivery-issue-notification">
<div className="module-delivery-issue-notification__first-line">
<span className="module-delivery-issue-notification__icon" />
<div className="SystemMessage SystemMessage--multiline">
<div className="SystemMessage__line">
<span className="SystemMessage__icon SystemMessage__icon--info" />
<Intl
id="DeliveryIssue--notification"
components={{
@ -56,13 +57,15 @@ export function DeliveryIssueNotification(
i18n={i18n}
/>
</div>
<button
type="button"
onClick={openDialog}
className="module-delivery-issue-notification__button"
>
{i18n('DeliveryIssue--learnMore')}
</button>
<div className="SystemMessage__line">
<Button
onClick={openDialog}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('DeliveryIssue--learnMore')}
</Button>
</div>
{isDialogOpen ? (
<DeliveryIssueDialog
i18n={i18n}

View file

@ -157,19 +157,21 @@ export class GroupNotification extends React.Component<Props> {
);
return (
<div className="module-group-notification">
{isLeftOnly ? null : (
<>
{fromLabel}
<br />
</>
)}
{(changes || []).map((change, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} className="module-group-notification__change">
{this.renderChange(change, from)}
</div>
))}
<div className="SystemMessage">
<div>
{isLeftOnly ? null : (
<>
{fromLabel}
<br />
</>
)}
{(changes || []).map((change, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} className="module-group-notification__change">
{this.renderChange(change, from)}
</div>
))}
</div>
</div>
);
}

View file

@ -3,6 +3,7 @@
import * as React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations';
import { Intl } from '../Intl';
@ -34,28 +35,40 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
}, [setShowingDialog]);
return (
<div className="module-group-v1-migration">
<div className="module-group-v1-migration--icon" />
<div className="module-group-v1-migration--text">
{i18n('GroupV1--Migration--was-upgraded')}
</div>
{areWeInvited ? (
<div className="module-group-v1-migration--text">
{i18n('GroupV1--Migration--invited--you')}
<div className="SystemMessage SystemMessage--multiline">
<div className="SystemMessage__line">
<div className="SystemMessage__icon SystemMessage__icon--group" />
<div>
<div>{i18n('GroupV1--Migration--was-upgraded')}</div>
<div>
{areWeInvited ? (
i18n('GroupV1--Migration--invited--you')
) : (
<>
{renderUsers(
invitedMembers,
i18n,
'GroupV1--Migration--invited'
)}
{renderUsers(
droppedMembers,
i18n,
'GroupV1--Migration--removed'
)}
</>
)}
</div>
</div>
) : (
<>
{renderUsers(invitedMembers, i18n, 'GroupV1--Migration--invited')}
{renderUsers(droppedMembers, i18n, 'GroupV1--Migration--removed')}
</>
)}
<button
type="button"
className="module-group-v1-migration--button"
onClick={showDialog}
>
{i18n('GroupV1--Migration--learn-more')}
</button>
</div>
<div className="SystemMessage__line">
<Button
onClick={showDialog}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('GroupV1--Migration--learn-more')}
</Button>
</div>
{showingDialog ? (
<GroupV1MigrationDialog
areWeInvited={areWeInvited}

View file

@ -55,10 +55,19 @@ storiesOf('Components/Conversation/GroupV2Change', module)
type: 'avatar',
removed: false,
},
{
type: 'description',
description:
'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
},
{
type: 'member-add',
conversationId: OUR_ID,
},
{
type: 'description',
description: 'Another description',
},
{
type: 'member-privilege',
conversationId: OUR_ID,

View file

@ -2,6 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactElement, useState } from 'react';
import classNames from 'classnames';
import { get } from 'lodash';
import { ReplacementValuesType } from '../../types/I18N';
import { FullJSXType, Intl } from '../Intl';
@ -9,7 +11,7 @@ import { LocalizerType } from '../../types/Util';
import { GroupDescriptionText } from '../GroupDescriptionText';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups';
import { GroupV2ChangeType, GroupV2ChangeDetailType } from '../../groups';
import { renderChange, SmartContactRendererType } from '../../groupChange';
import { Modal } from '../Modal';
@ -35,53 +37,141 @@ function renderStringToIntl(
return <Intl id={id} i18n={i18n} components={components} />;
}
export function GroupV2Change(props: PropsType): ReactElement {
const { change, groupName, i18n, ourConversationId, renderContact } = props;
type GroupIconType =
| 'group'
| 'group-access'
| 'group-add'
| 'group-approved'
| 'group-avatar'
| 'group-decline'
| 'group-edit'
| 'group-leave'
| 'group-remove';
const [
isGroupDescriptionDialogOpen,
setIsGroupDescriptionDialogOpen,
] = useState<boolean>(false);
const changeToIconMap = new Map<string, GroupIconType>([
['access-attributes', 'group-access'],
['access-invite-link', 'group-access'],
['access-members', 'group-access'],
['admin-approval-add-one', 'group-add'],
['admin-approval-remove-one', 'group-decline'],
['announcements-only', 'group-access'],
['avatar', 'group-avatar'],
['description', 'group-edit'],
['group-link-add', 'group-access'],
['group-link-remove', 'group-access'],
['group-link-reset', 'group-access'],
['member-add', 'group-add'],
['member-add-from-admin-approval', 'group-approved'],
['member-add-from-invite', 'group-add'],
['member-add-from-link', 'group-add'],
['member-privilege', 'group-access'],
['member-remove', 'group-remove'],
['pending-add-many', 'group-add'],
['pending-add-one', 'group-add'],
['pending-remove-many', 'group-decline'],
['pending-remove-one', 'group-decline'],
['title', 'group-edit'],
]);
const newGroupDescription = change.details.find(
(item): item is GroupV2DescriptionChangeType =>
Boolean(item.type === 'description' && item.description)
)?.description;
function getIcon(
detail: GroupV2ChangeDetailType,
fromId?: string
): GroupIconType {
const changeType = detail.type;
let possibleIcon = changeToIconMap.get(changeType);
const isSameId = fromId === get(detail, 'conversationId', null);
if (isSameId) {
if (changeType === 'member-remove') {
possibleIcon = 'group-leave';
}
if (changeType === 'member-add-from-invite') {
possibleIcon = 'group-approved';
}
}
return possibleIcon || 'group';
}
function GroupV2Detail({
detail,
i18n,
fromId,
onButtonClick,
text,
}: {
detail: GroupV2ChangeDetailType;
i18n: LocalizerType;
fromId?: string;
onButtonClick: (x: string) => unknown;
text: FullJSXType;
}): JSX.Element {
const icon = getIcon(detail, fromId);
const newGroupDescription =
detail.type === 'description' && get(detail, 'description');
return (
<div className="module-group-v2-change">
<div className="module-group-v2-change--icon" />
{renderChange(change, {
i18n,
ourConversationId,
renderContact,
renderString: renderStringToIntl,
}).map((item: FullJSXType, index: number) => (
// Difficult to find a unique key for this type
// eslint-disable-next-line react/no-array-index-key
<div key={index}>{item}</div>
))}
<div
className={classNames('SystemMessage', {
'SystemMessage--multiline': Boolean(newGroupDescription),
})}
>
<div className="SystemMessage__line">
<div className={`SystemMessage__icon SystemMessage__icon--${icon}`} />
<div className="SystemMessage__text">{text}</div>
</div>
{newGroupDescription ? (
<div className="module-group-v2-change--button-container">
<div className="SystemMessage__line">
<Button
onClick={() => onButtonClick(newGroupDescription)}
size={ButtonSize.Small}
variant={ButtonVariant.SecondaryAffirmative}
onClick={() => setIsGroupDescriptionDialogOpen(true)}
variant={ButtonVariant.SystemMessage}
>
{i18n('view')}
</Button>
</div>
) : null}
{newGroupDescription && isGroupDescriptionDialogOpen ? (
</div>
);
}
export function GroupV2Change(props: PropsType): ReactElement {
const { change, groupName, i18n, ourConversationId, renderContact } = props;
const [groupDescription, setGroupDescription] = useState<
string | undefined
>();
return (
<>
{renderChange(change, {
i18n,
ourConversationId,
renderContact,
renderString: renderStringToIntl,
}).map((text: FullJSXType, index: number) => (
<GroupV2Detail
detail={change.details[index]}
fromId={change.from}
i18n={i18n}
// Difficult to find a unique key for this type
// eslint-disable-next-line react/no-array-index-key
key={index}
onButtonClick={nextGroupDescription =>
setGroupDescription(nextGroupDescription)
}
text={text}
/>
))}
{groupDescription ? (
<Modal
hasXButton
i18n={i18n}
title={groupName}
onClose={() => setIsGroupDescriptionDialogOpen(false)}
onClose={() => setGroupDescription(undefined)}
>
<GroupDescriptionText text={newGroupDescription} />
<GroupDescriptionText text={groupDescription} />
</Modal>
) : null}
</div>
</>
);
}

View file

@ -22,8 +22,8 @@ export function ProfileChangeNotification(props: PropsType): JSX.Element {
const message = getStringForProfileChange(change, changedContact, i18n);
return (
<div className="module-profile-change-notification">
<div className="module-profile-change-notification--icon" />
<div className="SystemMessage">
<div className="SystemMessage__icon SystemMessage__icon--profile" />
<Emojify text={message} />
</div>
);

View file

@ -10,7 +10,5 @@ export type Props = {
};
export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => (
<div className="module-reset-session-notification">
{i18n('sessionEnded')}
</div>
<div className="SystemMessage">{i18n('sessionEnded')}</div>
);

View file

@ -3,6 +3,7 @@
import React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
@ -41,38 +42,42 @@ export const SafetyNumberNotification = ({
: 'safetyNumberChanged';
return (
<div className="module-safety-number-notification">
<div className="module-safety-number-notification__icon" />
<div className="module-safety-number-notification__text">
<Intl
id={changeKey}
components={[
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-safety-number-notification__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
<div className="SystemMessage SystemMessage--multiline">
<div className="SystemMessage__line">
<div className="SystemMessage__icon SystemMessage__icon--safety-number" />
<span>
<Intl
id={changeKey}
components={[
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-safety-number-notification__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</span>
</div>
<div className="SystemMessage__line">
<Button
onClick={() => {
showIdentity(contact.id);
}}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('verifyNewNumber')}
</Button>
</div>
<button
type="button"
onClick={() => {
showIdentity(contact.id);
}}
className="module-safety-number-notification__button"
>
{i18n('verifyNewNumber')}
</button>
</div>
);
};

View file

@ -118,19 +118,26 @@ storiesOf('Components/Conversation/TimelineItem', module)
type: 'fromOther',
},
},
{
type: 'universalTimerNotification',
data: null,
},
{
type: 'chatSessionRefreshed',
},
{
type: 'safetyNumberNotification',
data: {
isGroup: false,
contact: getDefaultConversation(),
},
},
{
type: 'deliveryIssue',
data: {
sender: getDefaultConversation(),
},
},
{
type: 'universalTimerNotification',
data: null,
},
{
type: 'changeNumberNotification',
data: {
@ -255,7 +262,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) outgoing audio
// unanswered (neither accepted nor declined) outgoing audio
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: false,
@ -266,7 +273,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) outgoing video
// unanswered (neither accepted nor declined) outgoing video
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: false,
@ -390,6 +397,71 @@ storiesOf('Components/Conversation/TimelineItem', module)
startedTime: Date.now(),
},
},
{
type: 'linkNotification',
data: null,
},
{
type: 'profileChange',
data: {
change: {
type: 'name',
oldName: 'Fred',
newName: 'John',
},
changedContact: getDefaultConversation(),
},
},
{
type: 'resetSessionNotification',
data: null,
},
{
type: 'unsupportedMessage',
data: {
canProcessNow: true,
contact: getDefaultConversation(),
},
},
{
type: 'unsupportedMessage',
data: {
canProcessNow: false,
contact: getDefaultConversation(),
},
},
{
type: 'verificationNotification',
data: {
type: 'markVerified',
isLocal: false,
contact: getDefaultConversation(),
},
},
{
type: 'verificationNotification',
data: {
type: 'markVerified',
isLocal: true,
contact: getDefaultConversation(),
},
},
{
type: 'verificationNotification',
data: {
type: 'markNotVerified',
isLocal: false,
contact: getDefaultConversation(),
},
},
{
type: 'verificationNotification',
data: {
type: 'markNotVerified',
isLocal: true,
contact: getDefaultConversation(),
},
},
];
return (

View file

@ -246,8 +246,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
);
} else if (item.type === 'linkNotification') {
notification = (
<div className="module-message-unsynced">
<div className="module-message-unsynced__icon" />
<div className="SystemMessage">
<div className="SystemMessage__icon SystemMessage__icon--unsynced" />
{i18n('messageHistoryUnsynced')}
</div>
);

View file

@ -95,17 +95,15 @@ export const TimerNotification: FunctionComponent<Props> = props => {
}
return (
<div className="module-timer-notification">
<div className="module-timer-notification__icon-container">
<div
className={classNames(
'module-timer-notification__icon',
disabled ? 'module-timer-notification__icon--disabled' : null
)}
/>
<div className="module-timer-notification__icon-label">{timespan}</div>
</div>
<div className="module-timer-notification__message">{message}</div>
<div className="SystemMessage">
<div
className={classNames(
'SystemMessage__icon',
'SystemMessage__icon--timer',
disabled ? 'SystemMessage__icon--timer-disabled' : null
)}
/>
<div>{message}</div>
</div>
);
};

View file

@ -11,8 +11,6 @@ export type Props = {
expireTimer: number;
};
const CSS_MODULE = 'module-universal-timer-notification';
export const UniversalTimerNotification: React.FC<Props> = props => {
const { i18n, expireTimer } = props;
@ -23,15 +21,14 @@ export const UniversalTimerNotification: React.FC<Props> = props => {
const timeValue = expirationTimer.format(i18n, expireTimer);
return (
<div className={CSS_MODULE}>
<div className={`${CSS_MODULE}__icon-container`}>
<div className={`${CSS_MODULE}__icon`} />
<div className={`${CSS_MODULE}__icon-label`}>{timeValue}</div>
</div>
<div className={`${CSS_MODULE}__message`}>
{i18n('UniversalTimerNotification__text', {
timeValue,
})}
<div className="SystemMessage">
<div className="SystemMessage__icon SystemMessage__icon--timer" />
<div className="SystemMessage__text">
<div>
{i18n('UniversalTimerNotification__text', {
timeValue,
})}
</div>
</div>
</div>
);

View file

@ -4,6 +4,7 @@
import React from 'react';
import classNames from 'classnames';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
@ -49,44 +50,51 @@ export const UnsupportedMessage = ({
const stringId = isMe ? meStringId : otherStringId;
return (
<div className="module-unsupported-message">
<div
className={classNames(
'module-unsupported-message__icon',
canProcessNow ? 'module-unsupported-message__icon--can-process' : null
)}
/>
<div className="module-unsupported-message__text">
<Intl
id={stringId}
components={[
<span
key="external-1"
className="module-unsupported-message__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-unsupported-message__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
<div className="SystemMessage SystemMessage--multiline">
<div className="SystemMessage__line SystemMessage__text">
<div
className={classNames(
'SystemMessage__icon',
'SystemMessage__icon--unsupported',
{
'SystemMessage__icon--unsupported--can-process': canProcessNow,
}
)}
/>
<span>
<Intl
id={stringId}
components={[
<span
key="external-1"
className="module-unsupported-message__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-unsupported-message__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</span>
</div>
{canProcessNow ? null : (
<button
type="button"
onClick={() => {
downloadNewVersion();
}}
className="module-unsupported-message__button"
>
{i18n('Message--update-signal')}
</button>
<div className="SystemMessage__line">
<Button
onClick={() => {
downloadNewVersion();
}}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('Message--update-signal')}
</Button>
</div>
)}
</div>
);

View file

@ -52,32 +52,33 @@ export class VerificationNotification extends React.Component<Props> {
const id = this.getStringId();
return (
<Intl
id={id}
components={[
<ContactName
key="external-1"
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-verification-notification__contact"
i18n={i18n}
/>,
]}
i18n={i18n}
/>
<div className="SystemMessage__text">
<Intl
id={id}
components={[
<ContactName
key="external-1"
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-verification-notification__contact"
i18n={i18n}
/>,
]}
i18n={i18n}
/>
</div>
);
}
public render(): JSX.Element {
const { type } = this.props;
const suffix =
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';
const suffix = type === 'markVerified' ? 'verified' : 'verified-not';
return (
<div className="module-verification-notification">
<div className={`module-verification-notification__icon--${suffix}`} />
<div className="SystemMessage">
<div className={`SystemMessage__icon SystemMessage__icon--${suffix}`} />
{this.renderContents()}
</div>
);