Styling adjustments to timeline notifications

This commit is contained in:
Evan Hahn 2021-09-07 14:55:03 -05:00 committed by GitHub
parent 4bed918cf8
commit 7f34bedd87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 437 additions and 360 deletions

View file

@ -2060,7 +2060,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
.module-inline-notification-wrapper { .module-inline-notification-wrapper {
outline: none; outline: none;
padding: 5px;
&:focus { &:focus {
@include keyboard-mode { @include keyboard-mode {
@ -2074,11 +2073,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
// Module: Group Notification // Module: Group Notification
.module-group-notification__change {
margin-top: 2px;
margin-bottom: 2px;
}
.module-group-notification__contact { .module-group-notification__contact {
font-weight: bold; font-weight: bold;
} }

View file

@ -160,6 +160,14 @@
&--system-message { &--system-message {
@include rounded-corners; @include rounded-corners;
&.module-Button--small {
padding: {
top: 5px;
bottom: 5px;
}
font-weight: 500;
}
@include light-theme { @include light-theme {
$color: $color-ultramarine; $color: $color-ultramarine;
$background-color: $color-gray-02; $background-color: $color-gray-02;

View file

@ -16,245 +16,259 @@
@include font-body-2; @include font-body-2;
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: center; justify-content: center;
text-align: center; line-height: 16px;
margin-bottom: 16px;
margin-top: 16px;
@include light-theme { @include light-theme {
color: $color-black; color: $color-gray-60;
} }
@include dark-theme { @include dark-theme {
color: $color-gray-25; color: $color-gray-25;
} }
&--multiline { &__contents {
flex-direction: column;
}
&__line {
align-items: center;
display: flex;
&:not(:first-child) {
margin-top: 12px;
}
}
&__text {
max-width: 400px; max-width: 400px;
} text-align: center;
user-select: none;
&__icon { p {
height: 16px; display: block;
flex: 0 0 16px; margin: {
margin-right: 8px; block: {
width: 16px; start: 0;
vertical-align: middle; end: 4px;
}
inline: {
start: 0;
end: 0;
}
}
@include light-theme { &:first-child {
background-color: $color-black; display: inline-block;
} }
@include dark-theme {
background-color: $color-gray-25; &:last-child {
margin-block-end: 0;
}
} }
&--audio-incoming { &::before {
content: '';
display: inline-block;
height: 16px;
margin-right: 8px;
width: 16px;
vertical-align: middle;
@include light-theme {
background-color: $color-gray-60;
}
@include dark-theme {
background-color: $color-gray-25;
}
}
&--icon-audio-incoming::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/phone-incoming-16.svg', '../images/icons/v2/phone-incoming-16.svg',
'../images/icons/v2/phone-incoming-solid-16.svg' '../images/icons/v2/phone-incoming-solid-16.svg'
); );
} }
&--audio-missed { &--icon-audio-missed::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/phone-x-16.svg', '../images/icons/v2/phone-x-16.svg',
'../images/icons/v2/phone-x-solid-16.svg' '../images/icons/v2/phone-x-solid-16.svg'
); );
} }
&--audio-outgoing { &--icon-audio-outgoing::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/phone-outgoing-16.svg', '../images/icons/v2/phone-outgoing-16.svg',
'../images/icons/v2/phone-outgoing-solid-16.svg' '../images/icons/v2/phone-outgoing-solid-16.svg'
); );
} }
&--group { &--icon-group::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/group-outline-24.svg', '../images/icons/v2/group-outline-24.svg',
'../images/icons/v2/group-solid-24.svg' '../images/icons/v2/group-solid-24.svg'
); );
} }
&--group-access { &--icon-group-access::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/megaphone-16.svg', '../images/icons/v2/megaphone-16.svg',
'../images/icons/v2/megaphone-solid-16.svg' '../images/icons/v2/megaphone-solid-16.svg'
); );
} }
&--group-add { &--icon-group-add::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/member-added-16.svg', '../images/icons/v2/member-added-16.svg',
'../images/icons/v2/member-added-solid-16.svg' '../images/icons/v2/member-added-solid-16.svg'
); );
} }
&--group-approved { &--icon-group-approved::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/member-accepted-16.svg', '../images/icons/v2/member-accepted-16.svg',
'../images/icons/v2/member-accepted-solid-16.svg' '../images/icons/v2/member-accepted-solid-16.svg'
); );
} }
&--group-avatar { &--icon-group-avatar::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/photo-16.svg', '../images/icons/v2/photo-16.svg',
'../images/icons/v2/photo-solid-16.svg' '../images/icons/v2/photo-solid-16.svg'
); );
} }
&--group-decline { &--icon-group-decline::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/member-declined-16.svg', '../images/icons/v2/member-declined-16.svg',
'../images/icons/v2/member-declined-solid-16.svg' '../images/icons/v2/member-declined-solid-16.svg'
); );
} }
&--group-edit { &--icon-group-edit::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/edit-16.svg', '../images/icons/v2/edit-16.svg',
'../images/icons/v2/edit-solid-16.svg' '../images/icons/v2/edit-solid-16.svg'
); );
} }
&--group-leave { &--icon-group-leave::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/leave-16.svg', '../images/icons/v2/leave-16.svg',
'../images/icons/v2/leave-solid-16.svg' '../images/icons/v2/leave-solid-16.svg'
); );
} }
&--group-remove { &--icon-group-remove::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/member-remove-16.svg', '../images/icons/v2/member-remove-16.svg',
'../images/icons/v2/member-remove-solid-16.svg' '../images/icons/v2/member-remove-solid-16.svg'
); );
} }
&--info { &--icon-info::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/info-16.svg', '../images/icons/v2/info-16.svg',
'../images/icons/v2/info-solid-24.svg' '../images/icons/v2/info-solid-24.svg'
); );
} }
&--phone { &--icon-phone::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/phone-right-outline-24.svg', '../images/icons/v2/phone-right-outline-24.svg',
'../images/icons/v2/phone-right-solid-24.svg' '../images/icons/v2/phone-right-solid-24.svg'
); );
} }
&--profile { &--icon-profile::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/profile-outline-20.svg', '../images/icons/v2/profile-outline-20.svg',
'../images/icons/v2/profile-outline-20.svg' '../images/icons/v2/profile-outline-20.svg'
); );
} }
&--safety-number { &--icon-safety-number::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/safety-number-outline-24.svg', '../images/icons/v2/safety-number-outline-24.svg',
'../images/icons/v2/safety-number-solid-24.svg' '../images/icons/v2/safety-number-solid-24.svg'
); );
} }
&--session-refresh { &--icon-session-refresh::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/refresh-16.svg', '../images/icons/v2/refresh-16.svg',
'../images/icons/v2/refresh-16.svg' '../images/icons/v2/refresh-16.svg'
); );
} }
&--timer { &--icon-timer::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/timer-outline-24.svg', '../images/icons/v2/timer-outline-24.svg',
'../images/icons/v2/timer-solid-16.svg' '../images/icons/v2/timer-solid-16.svg'
); );
} }
&--timer-disabled { &--icon-timer-disabled::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/timer-disabled-outline-24.svg', '../images/icons/v2/timer-disabled-outline-24.svg',
'../images/icons/v2/timer-disabled-solid-16.svg' '../images/icons/v2/timer-disabled-solid-16.svg'
); );
} }
&--unsupported { &--icon-unsupported::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/error-outline-24.svg', '../images/icons/v2/error-outline-24.svg',
'../images/icons/v2/error-solid-24.svg' '../images/icons/v2/error-solid-24.svg'
); );
&--can-process {
@include system-message-icon(
'../images/icons/v2/check-circle-outline-24.svg',
'../images/icons/v2/check-circle-solid-24.svg'
);
}
} }
&--unsynced { &--icon-unsupported--can-process::before {
@include system-message-icon(
'../images/icons/v2/check-circle-outline-24.svg',
'../images/icons/v2/check-circle-solid-24.svg'
);
}
&--icon-unsynced::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/info-outline-24.svg', '../images/icons/v2/info-outline-24.svg',
'../images/icons/v2/info-solid-24.svg' '../images/icons/v2/info-solid-24.svg'
); );
} }
&--verified { &--icon-verified::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/check-24.svg', '../images/icons/v2/check-24.svg',
'../images/icons/v2/check-24.svg' '../images/icons/v2/check-24.svg'
); );
} }
&--verified-not { &--icon-verified-not::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/safety-number-outline-24.svg', '../images/icons/v2/safety-number-outline-24.svg',
'../images/icons/v2/safety-number-solid-24.svg' '../images/icons/v2/safety-number-solid-24.svg'
); );
} }
&--video { &--icon-video::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/video-outline-24.svg', '../images/icons/v2/video-outline-24.svg',
'../images/icons/v2/video-outline-24.svg' '../images/icons/v2/video-outline-24.svg'
); );
} }
&--video-incoming { &--icon-video-incoming::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/video-incoming-16.svg', '../images/icons/v2/video-incoming-16.svg',
'../images/icons/v2/video-incoming-solid-16.svg' '../images/icons/v2/video-incoming-solid-16.svg'
); );
} }
&--video-missed { &--icon-video-missed::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/video-x-16.svg', '../images/icons/v2/video-x-16.svg',
'../images/icons/v2/video-x-solid-16.svg' '../images/icons/v2/video-x-solid-16.svg'
); );
} }
&--video-outgoing { &--icon-video-outgoing::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/video-outgoing-16.svg', '../images/icons/v2/video-outgoing-16.svg',
'../images/icons/v2/video-outgoing-solid-16.svg' '../images/icons/v2/video-outgoing-solid-16.svg'
); );
} }
&--warning { &--icon-warning::before {
@include system-message-icon( @include system-message-icon(
'../images/icons/v2/error-outline-12.svg', '../images/icons/v2/error-outline-12.svg',
'../images/icons/v2/error-outline-12.svg' '../images/icons/v2/error-outline-12.svg'
@ -265,13 +279,12 @@
&--error { &--error {
color: $color-accent-red; color: $color-accent-red;
.SystemMessage__icon { .SystemMessage__contents::before {
background: $color-accent-red; background: $color-accent-red;
} }
} }
img.emoji { &__button-container {
// The negative bottom margin offset doesn't play well with align-items center margin-top: 12px;
margin-bottom: 0;
} }
} }

View file

@ -2,10 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { SystemMessage } from './SystemMessage';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { Timestamp } from './Timestamp'; import { Timestamp } from './Timestamp';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -86,17 +86,12 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
}} }}
> >
{({ measureRef }) => ( {({ measureRef }) => (
<div <SystemMessage
className={classNames('SystemMessage', 'SystemMessage--multiline', { button={
'SystemMessage--error': wasMissed, hasButton ? <CallingNotificationButton {...props} /> : undefined
})} }
ref={measureRef} contents={
> <>
<div className="SystemMessage__line">
<div
className={`SystemMessage__icon SystemMessage__icon--${icon}`}
/>
<div>
{getCallingNotificationText(props, i18n)} &middot;{' '} {getCallingNotificationText(props, i18n)} &middot;{' '}
<Timestamp <Timestamp
direction="outgoing" direction="outgoing"
@ -107,14 +102,12 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
withSticker={false} withSticker={false}
withTapToViewExpired={false} withTapToViewExpired={false}
/> />
</div> </>
</div> }
{hasButton ? ( icon={icon}
<div className="SystemMessage__line"> isError={wasMissed}
<CallingNotificationButton {...props} /> ref={measureRef}
</div> />
) : null}
</div>
)} )}
</Measure> </Measure>
); );

View file

@ -0,0 +1,36 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { ChangeNumberNotification } from './ChangeNumberNotification';
const story = storiesOf(
'Components/Conversation/ChangeNumberNotification',
module
);
const i18n = setupI18n('en', enMessages);
story.add('Default', () => (
<ChangeNumberNotification
sender={getDefaultConversation()}
timestamp={1618894800000}
i18n={i18n}
/>
));
story.add('Long name', () => (
<ChangeNumberNotification
sender={getDefaultConversation({
firstName: '💅😇🖋'.repeat(50),
})}
timestamp={1618894800000}
i18n={i18n}
/>
));

View file

@ -7,6 +7,7 @@ import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { SystemMessage } from './SystemMessage';
import { Timestamp } from './Timestamp'; import { Timestamp } from './Timestamp';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
@ -25,17 +26,21 @@ export const ChangeNumberNotification: React.FC<Props> = props => {
const { i18n, sender, timestamp } = props; const { i18n, sender, timestamp } = props;
return ( return (
<div className="SystemMessage"> <SystemMessage
<span className="SystemMessage__icon SystemMessage__icon--phone" /> contents={
<Intl <>
id="ChangeNumber--notification" <Intl
components={{ id="ChangeNumber--notification"
sender: <Emojify text={sender.firstName || sender.title} />, components={{
}} sender: <Emojify text={sender.firstName || sender.title} />,
i18n={i18n} }}
/> i18n={i18n}
&nbsp;·&nbsp; />
<Timestamp i18n={i18n} timestamp={timestamp} /> &nbsp;·&nbsp;
</div> <Timestamp i18n={i18n} timestamp={timestamp} />
</>
}
icon="phone"
/>
); );
}; };

View file

@ -6,6 +6,7 @@ import React, { useCallback, useState, ReactElement } from 'react';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog'; import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
type PropsHousekeepingType = { type PropsHousekeepingType = {
@ -37,20 +38,20 @@ export function ChatSessionRefreshedNotification(
}, [contactSupport, setIsDialogOpen]); }, [contactSupport, setIsDialogOpen]);
return ( return (
<div className="SystemMessage SystemMessage--multiline"> <>
<div className="SystemMessage__line"> <SystemMessage
<span className="SystemMessage__icon SystemMessage__icon--session-refresh" /> contents={i18n('ChatRefresh--notification')}
{i18n('ChatRefresh--notification')} button={
</div> <Button
<div className="SystemMessage__line"> onClick={openDialog}
<Button size={ButtonSize.Small}
onClick={openDialog} variant={ButtonVariant.SystemMessage}
size={ButtonSize.Small} >
variant={ButtonVariant.SystemMessage} {i18n('ChatRefresh--learnMore')}
> </Button>
{i18n('ChatRefresh--learnMore')} }
</Button> icon="session-refresh"
</div> />
{isDialogOpen ? ( {isDialogOpen ? (
<ChatSessionRefreshedDialog <ChatSessionRefreshedDialog
onClose={closeDialog} onClose={closeDialog}
@ -58,6 +59,6 @@ export function ChatSessionRefreshedNotification(
i18n={i18n} i18n={i18n}
/> />
) : null} ) : null}
</div> </>
); );
} }

View file

@ -4,6 +4,7 @@
import React, { useCallback, useState, ReactElement } from 'react'; import React, { useCallback, useState, ReactElement } from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { ConversationType } from '../../state/ducks/conversations'; import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
@ -46,10 +47,9 @@ export function DeliveryIssueNotification(
} }
return ( return (
<div className="SystemMessage SystemMessage--multiline"> <>
<div className="SystemMessage__line"> <SystemMessage
<span className="SystemMessage__icon SystemMessage__icon--info" /> contents={
<span>
<Intl <Intl
id="DeliveryIssue--notification" id="DeliveryIssue--notification"
components={{ components={{
@ -57,17 +57,18 @@ export function DeliveryIssueNotification(
}} }}
i18n={i18n} i18n={i18n}
/> />
</span> }
</div> icon="info"
<div className="SystemMessage__line"> button={
<Button <Button
onClick={openDialog} onClick={openDialog}
size={ButtonSize.Small} size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage} variant={ButtonVariant.SystemMessage}
> >
{i18n('DeliveryIssue--learnMore')} {i18n('DeliveryIssue--learnMore')}
</Button> </Button>
</div> }
/>
{isDialogOpen ? ( {isDialogOpen ? (
<DeliveryIssueDialog <DeliveryIssueDialog
i18n={i18n} i18n={i18n}
@ -77,6 +78,6 @@ export function DeliveryIssueNotification(
onClose={closeDialog} onClose={closeDialog}
/> />
) : null} ) : null}
</div> </>
); );
} }

View file

@ -1,10 +1,11 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { ReactNode } from 'react';
import { compact, flatten } from 'lodash'; import { compact, flatten } from 'lodash';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { SystemMessage } from './SystemMessage';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -133,12 +134,15 @@ export class GroupNotification extends React.Component<Props> {
} }
public render(): JSX.Element { public render(): JSX.Element {
const { changes, i18n, from } = this.props; const { changes: rawChanges, i18n, from } = this.props;
// This check is just to be extra careful, and can probably be removed.
const changes: Array<Change> = Array.isArray(rawChanges) ? rawChanges : [];
// Leave messages are always from the person leaving, so we omit the fromLabel if // Leave messages are always from the person leaving, so we omit the fromLabel if
// the change is a 'leave.' // the change is a 'leave.'
const isLeftOnly = const firstChange: undefined | Change = changes[0];
changes && changes.length === 1 && changes[0].type === 'remove'; const isLeftOnly = changes.length === 1 && firstChange?.type === 'remove';
const fromContact = ( const fromContact = (
<ContactName <ContactName
@ -156,23 +160,23 @@ export class GroupNotification extends React.Component<Props> {
<Intl i18n={i18n} id="updatedTheGroup" components={[fromContact]} /> <Intl i18n={i18n} id="updatedTheGroup" components={[fromContact]} />
); );
return ( let contents: ReactNode;
<div className="SystemMessage"> if (isLeftOnly) {
<div> contents = this.renderChange(firstChange, from);
{isLeftOnly ? null : ( } else {
<> contents = (
{fromLabel} <>
<br /> <p>{fromLabel}</p>
</> {changes.map((change, i) => (
)}
{(changes || []).map((change, i) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<div key={i} className="module-group-notification__change"> <p key={i} className="module-group-notification__change">
{this.renderChange(change, from)} {this.renderChange(change, from)}
</div> </p>
))} ))}
</div> </>
</div> );
); }
return <SystemMessage contents={contents} icon="group" />;
} }
} }

View file

@ -1,9 +1,10 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations'; import { ConversationType } from '../../state/ducks/conversations';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
@ -35,40 +36,42 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
}, [setShowingDialog]); }, [setShowingDialog]);
return ( return (
<div className="SystemMessage SystemMessage--multiline"> <>
<div className="SystemMessage__line"> <SystemMessage
<div className="SystemMessage__icon SystemMessage__icon--group" /> icon="group"
<div> contents={
<div>{i18n('GroupV1--Migration--was-upgraded')}</div> <>
<div> <p>{i18n('GroupV1--Migration--was-upgraded')}</p>
{areWeInvited ? ( <p>
i18n('GroupV1--Migration--invited--you') {areWeInvited ? (
) : ( i18n('GroupV1--Migration--invited--you')
<> ) : (
{renderUsers( <>
invitedMembers, {renderUsers(
i18n, invitedMembers,
'GroupV1--Migration--invited' i18n,
)} 'GroupV1--Migration--invited'
{renderUsers( )}
droppedMembers, {renderUsers(
i18n, droppedMembers,
'GroupV1--Migration--removed' i18n,
)} 'GroupV1--Migration--removed'
</> )}
)} </>
</div> )}
</div> </p>
</div> </>
<div className="SystemMessage__line"> }
<Button button={
onClick={showDialog} <Button
size={ButtonSize.Small} onClick={showDialog}
variant={ButtonVariant.SystemMessage} size={ButtonSize.Small}
> variant={ButtonVariant.SystemMessage}
{i18n('GroupV1--Migration--learn-more')} >
</Button> {i18n('GroupV1--Migration--learn-more')}
</div> </Button>
}
/>
{showingDialog ? ( {showingDialog ? (
<GroupV1MigrationDialog <GroupV1MigrationDialog
areWeInvited={areWeInvited} areWeInvited={areWeInvited}
@ -82,7 +85,7 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
onClose={dismissDialog} onClose={dismissDialog}
/> />
) : null} ) : null}
</div> </>
); );
} }
@ -95,23 +98,17 @@ function renderUsers(
return null; return null;
} }
const className = 'module-group-v1-migration--text';
if (members.length === 1) { if (members.length === 1) {
return ( return (
<div className={className}> <p>
<Intl <Intl
i18n={i18n} i18n={i18n}
id={`${keyPrefix}--one`} id={`${keyPrefix}--one`}
components={[<ContactName title={members[0].title} i18n={i18n} />]} components={[<ContactName title={members[0].title} i18n={i18n} />]}
/> />
</div> </p>
); );
} }
return ( return <p>{i18n(`${keyPrefix}--many`, [members.length.toString()])}</p>;
<div className={className}>
{i18n(`${keyPrefix}--many`, [members.length.toString()])}
</div>
);
} }

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from 'react';
import classNames from 'classnames';
import { get } from 'lodash'; import { get } from 'lodash';
import { ReplacementValuesType } from '../../types/I18N'; import { ReplacementValuesType } from '../../types/I18N';
@ -10,6 +9,7 @@ import { FullJSXType, Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { GroupDescriptionText } from '../GroupDescriptionText'; import { GroupDescriptionText } from '../GroupDescriptionText';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { GroupV2ChangeType, GroupV2ChangeDetailType } from '../../groups'; import { GroupV2ChangeType, GroupV2ChangeDetailType } from '../../groups';
@ -110,17 +110,11 @@ function GroupV2Detail({
detail.type === 'description' && get(detail, 'description'); detail.type === 'description' && get(detail, 'description');
return ( return (
<div <SystemMessage
className={classNames('SystemMessage', { icon={icon}
'SystemMessage--multiline': Boolean(newGroupDescription), contents={text}
})} button={
> newGroupDescription ? (
<div className="SystemMessage__line">
<div className={`SystemMessage__icon SystemMessage__icon--${icon}`} />
<div className="SystemMessage__text">{text}</div>
</div>
{newGroupDescription ? (
<div className="SystemMessage__line">
<Button <Button
onClick={() => onButtonClick(newGroupDescription)} onClick={() => onButtonClick(newGroupDescription)}
size={ButtonSize.Small} size={ButtonSize.Small}
@ -128,9 +122,9 @@ function GroupV2Detail({
> >
{i18n('view')} {i18n('view')}
</Button> </Button>
</div> ) : undefined
) : null} }
</div> />
); );
} }

View file

@ -0,0 +1,16 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { LinkNotification } from './LinkNotification';
const story = storiesOf('Components/Conversation/LinkNotification', module);
const i18n = setupI18n('en', enMessages);
story.add('Default', () => <LinkNotification i18n={i18n} />);

View file

@ -0,0 +1,13 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { SystemMessage } from './SystemMessage';
import type { LocalizerType } from '../../types/Util';
export const LinkNotification = ({
i18n,
}: Readonly<{ i18n: LocalizerType }>): JSX.Element => (
<SystemMessage icon="unsynced" contents={i18n('messageHistoryUnsynced')} />
);

View file

@ -1,10 +1,11 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations'; import { ConversationType } from '../../state/ducks/conversations';
import { SystemMessage } from './SystemMessage';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { import {
getStringForProfileChange, getStringForProfileChange,
@ -21,12 +22,5 @@ export function ProfileChangeNotification(props: PropsType): JSX.Element {
const { change, changedContact, i18n } = props; const { change, changedContact, i18n } = props;
const message = getStringForProfileChange(change, changedContact, i18n); const message = getStringForProfileChange(change, changedContact, i18n);
return ( return <SystemMessage icon="profile" contents={<Emojify text={message} />} />;
<div className="SystemMessage">
<div className="SystemMessage__icon SystemMessage__icon--profile" />
<span>
<Emojify text={message} />
</span>
</div>
);
} }

View file

@ -1,14 +1,15 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { SystemMessage } from './SystemMessage';
export type Props = { export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => ( export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => (
<div className="SystemMessage">{i18n('sessionEnded')}</div> <SystemMessage contents={i18n('sessionEnded')} icon="session-refresh" />
); );

View file

@ -1,9 +1,10 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -42,32 +43,30 @@ export const SafetyNumberNotification = ({
: 'safetyNumberChanged'; : 'safetyNumberChanged';
return ( return (
<div className="SystemMessage SystemMessage--multiline"> <SystemMessage
<div className="SystemMessage__line"> icon="safety-number"
<div className="SystemMessage__icon SystemMessage__icon--safety-number" /> contents={
<span> <Intl
<Intl id={changeKey}
id={changeKey} components={[
components={[ <span
<span key="external-1"
key="external-1" className="module-safety-number-notification__contact"
className="module-safety-number-notification__contact" >
> <ContactName
<ContactName name={contact.name}
name={contact.name} profileName={contact.profileName}
profileName={contact.profileName} phoneNumber={contact.phoneNumber}
phoneNumber={contact.phoneNumber} title={contact.title}
title={contact.title} module="module-safety-number-notification__contact"
module="module-safety-number-notification__contact" i18n={i18n}
i18n={i18n} />
/> </span>,
</span>, ]}
]} i18n={i18n}
i18n={i18n} />
/> }
</span> button={
</div>
<div className="SystemMessage__line">
<Button <Button
onClick={() => { onClick={() => {
showIdentity(contact.id); showIdentity(contact.id);
@ -77,7 +76,7 @@ export const SafetyNumberNotification = ({
> >
{i18n('verifyNewNumber')} {i18n('verifyNewNumber')}
</Button> </Button>
</div> }
</div> />
); );
}; };

View file

@ -0,0 +1,38 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactNode, forwardRef } from 'react';
import classNames from 'classnames';
type PropsType = {
icon: string;
contents: ReactNode;
button?: ReactNode;
isError?: boolean;
};
export const SystemMessage = forwardRef<HTMLDivElement, PropsType>(
({ icon, contents, button, isError }, ref) => {
return (
<div
className={classNames(
'SystemMessage',
isError && 'SystemMessage--error'
)}
ref={ref}
>
<div
className={classNames(
'SystemMessage__contents',
`SystemMessage__contents--icon-${icon}`
)}
>
{contents}
</div>
{button && (
<div className="SystemMessage__button-container">{button}</div>
)}
</div>
);
}
);

View file

@ -26,6 +26,7 @@ import {
PropsActionsType as DeliveryIssueActionProps, PropsActionsType as DeliveryIssueActionProps,
PropsDataType as DeliveryIssueProps, PropsDataType as DeliveryIssueProps,
} from './DeliveryIssueNotification'; } from './DeliveryIssueNotification';
import { LinkNotification } from './LinkNotification';
import { import {
ChangeNumberNotification, ChangeNumberNotification,
PropsData as ChangeNumberNotificationProps, PropsData as ChangeNumberNotificationProps,
@ -245,12 +246,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
<DeliveryIssueNotification {...item.data} {...this.props} i18n={i18n} /> <DeliveryIssueNotification {...item.data} {...this.props} i18n={i18n} />
); );
} else if (item.type === 'linkNotification') { } else if (item.type === 'linkNotification') {
notification = ( notification = <LinkNotification i18n={i18n} />;
<div className="SystemMessage">
<div className="SystemMessage__icon SystemMessage__icon--unsynced" />
{i18n('messageHistoryUnsynced')}
</div>
);
} else if (item.type === 'timerNotification') { } else if (item.type === 'timerNotification') {
notification = ( notification = (
<TimerNotification {...this.props} {...item.data} i18n={i18n} /> <TimerNotification {...this.props} {...item.data} i18n={i18n} />

View file

@ -2,9 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { FunctionComponent, ReactNode } from 'react'; import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { SystemMessage } from './SystemMessage';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer'; import * as expirationTimer from '../../util/expirationTimer';
@ -94,16 +94,7 @@ export const TimerNotification: FunctionComponent<Props> = props => {
break; break;
} }
return ( const icon = disabled ? 'timer-disabled' : 'timer';
<div className="SystemMessage">
<div return <SystemMessage icon={icon} contents={message} />;
className={classNames(
'SystemMessage__icon',
'SystemMessage__icon--timer',
disabled ? 'SystemMessage__icon--timer-disabled' : null
)}
/>
<div>{message}</div>
</div>
);
}; };

View file

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import { SystemMessage } from './SystemMessage';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer'; import * as expirationTimer from '../../util/expirationTimer';
@ -21,15 +22,11 @@ export const UniversalTimerNotification: React.FC<Props> = props => {
const timeValue = expirationTimer.format(i18n, expireTimer); const timeValue = expirationTimer.format(i18n, expireTimer);
return ( return (
<div className="SystemMessage"> <SystemMessage
<div className="SystemMessage__icon SystemMessage__icon--timer" /> icon="timer"
<div className="SystemMessage__text"> contents={i18n('UniversalTimerNotification__text', {
<div> timeValue,
{i18n('UniversalTimerNotification__text', { })}
timeValue, />
})}
</div>
</div>
</div>
); );
}; };

View file

@ -1,9 +1,9 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { SystemMessage } from './SystemMessage';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
@ -48,54 +48,47 @@ export const UnsupportedMessage = ({
? 'Message--from-me-unsupported-message-ask-to-resend' ? 'Message--from-me-unsupported-message-ask-to-resend'
: 'Message--from-me-unsupported-message'; : 'Message--from-me-unsupported-message';
const stringId = isMe ? meStringId : otherStringId; const stringId = isMe ? meStringId : otherStringId;
const icon = canProcessNow ? 'unsupported--can-process' : 'unsupported';
return ( return (
<div className="SystemMessage SystemMessage--multiline"> <SystemMessage
<div className="SystemMessage__line SystemMessage__text"> icon={icon}
<div contents={
className={classNames( <Intl
'SystemMessage__icon', id={stringId}
'SystemMessage__icon--unsupported', components={[
{ <span
'SystemMessage__icon--unsupported--can-process': canProcessNow, 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> }
<Intl button={
id={stringId} canProcessNow ? undefined : (
components={[ <div className="SystemMessage__line">
<span <Button
key="external-1" onClick={() => {
className="module-unsupported-message__contact" downloadNewVersion();
> }}
<ContactName size={ButtonSize.Small}
name={contact.name} variant={ButtonVariant.SystemMessage}
profileName={contact.profileName} >
phoneNumber={contact.phoneNumber} {i18n('Message--update-signal')}
title={contact.title} </Button>
module="module-unsupported-message__contact" </div>
i18n={i18n} )
/> }
</span>, />
]}
i18n={i18n}
/>
</span>
</div>
{canProcessNow ? null : (
<div className="SystemMessage__line">
<Button
onClick={() => {
downloadNewVersion();
}}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}
>
{i18n('Message--update-signal')}
</Button>
</div>
)}
</div>
); );
}; };

View file

@ -1,9 +1,9 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
// import classNames from 'classnames';
import { SystemMessage } from './SystemMessage';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -52,35 +52,28 @@ export class VerificationNotification extends React.Component<Props> {
const id = this.getStringId(); const id = this.getStringId();
return ( return (
<div className="SystemMessage__text"> <Intl
<Intl id={id}
id={id} components={[
components={[ <ContactName
<ContactName key="external-1"
key="external-1" name={contact.name}
name={contact.name} profileName={contact.profileName}
profileName={contact.profileName} phoneNumber={contact.phoneNumber}
phoneNumber={contact.phoneNumber} title={contact.title}
title={contact.title} module="module-verification-notification__contact"
module="module-verification-notification__contact" i18n={i18n}
i18n={i18n} />,
/>, ]}
]} i18n={i18n}
i18n={i18n} />
/>
</div>
); );
} }
public render(): JSX.Element { public render(): JSX.Element {
const { type } = this.props; const { type } = this.props;
const suffix = type === 'markVerified' ? 'verified' : 'verified-not'; const icon = type === 'markVerified' ? 'verified' : 'verified-not';
return ( return <SystemMessage icon={icon} contents={this.renderContents()} />;
<div className="SystemMessage">
<div className={`SystemMessage__icon SystemMessage__icon--${suffix}`} />
{this.renderContents()}
</div>
);
} }
} }