Show "no groups in common" warning for relevant message requests

This commit is contained in:
Evan Hahn 2021-04-30 17:58:57 -05:00 committed by GitHub
parent 05703c2719
commit fe772af251
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 122 deletions

View file

@ -3142,6 +3142,10 @@
"message": "No groups in common",
"description": "Shown to indicate this user is not a member of any groups"
},
"no-groups-in-common-warning": {
"message": "No groups in common. Review requests carefully.",
"description": "When a user has no common groups, show this warning"
},
"acceptCall": {
"message": "Answer",
"description": "Shown in tooltip for the button to accept a call (audio or video)"
@ -5165,6 +5169,18 @@
"message": "Continue",
"description": "aria-label for the 'next' button in the forward a message modal dialog"
},
"MessageRequestWarning__learn-more": {
"message": "Learn more",
"description": "Shown on the message request warning. Clicking this button will open a dialog with more information"
},
"MessageRequestWarning__dialog__details": {
"message": "You have no groups in common with this person. Review requests carefully before accepting to avoid unwanted messages.",
"description": "Shown in the message request warning dialog. Gives more information about message requests"
},
"MessageRequestWarning__dialog__learn-even-more": {
"message": "About Message Requests",
"description": "Shown in the message request warning dialog. Clicking this button will open a page on Signal's support site"
},
"ContactSpoofing__same-name": {
"message": "Review requests carefully. Signal found another contact with the same name. $link$",
"description": "Shown in the timeline warning when you have a message request from someone with the same name as someone else",

View file

@ -106,7 +106,12 @@
}
}
// Smooth scrolling
// Utilities
@mixin rounded-corners() {
// This ensures the borders are completely rounded. (A value like 100% would make it an ellipse.)
border-radius: 9999px;
}
@mixin smooth-scroll() {
scroll-behavior: smooth;
@ -472,7 +477,7 @@
}
@mixin button-small {
border-radius: 9999px; // This ensures the borders are completely rounded. (A value like 100% would make it an ellipse.)
@include rounded-corners;
padding: 7px 14px;
}

View file

@ -3934,6 +3934,46 @@ button.module-conversation-details__action-button {
@include font-body-2-bold;
}
}
&__message-request-warning {
@include font-body-2;
&__message {
display: flex;
margin-bottom: 12px;
align-items: center;
justify-content: center;
user-select: none;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
&::before {
content: '';
display: block;
height: 14px;
margin-right: 8px;
width: 14px;
@include light-theme {
@include color-svg(
'../images/icons/v2/info-outline-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/info-solid-24.svg',
$color-gray-25
);
}
}
}
}
}
// Module: Message Request Actions

View file

@ -18,8 +18,6 @@
}
@include button-reset;
@include font-body-1-bold;
border-radius: 4px;
padding: 8px 16px;
text-align: center;
@ -37,6 +35,16 @@
cursor: not-allowed;
}
&--medium {
@include font-body-1-bold;
}
&--small {
@include font-body-2;
@include rounded-corners;
padding: 6px 12px;
}
&--primary {
$color: $color-white;
$background-color: $ultramarine-ui-light;

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
.module-ContactPill {
@include rounded-corners;
align-items: center;
border-radius: 9999px; // This ensures the borders are completely rounded. (A value like 100% would make it an ellipse.)
display: inline-flex;
user-select: none;
overflow: hidden;

View file

@ -279,9 +279,9 @@
&--join-call {
@include font-body-1;
@include rounded-corners;
align-items: center;
background-color: $color-accent-green;
border-radius: 9999px; // This ensures the borders are completely rounded. (A value like 100% would make it an ellipse.)
color: $color-white;
display: flex;
outline: none;

View file

@ -5,30 +5,39 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Button, ButtonVariant } from './Button';
import { Button, ButtonSize, ButtonVariant } from './Button';
const story = storiesOf('Components/Button', module);
story.add('Kitchen sink', () => (
<>
{[
ButtonVariant.Primary,
ButtonVariant.Secondary,
ButtonVariant.SecondaryAffirmative,
ButtonVariant.SecondaryDestructive,
ButtonVariant.Destructive,
].map(variant => (
<React.Fragment key={variant}>
<p>
<Button onClick={action('onClick')} variant={variant}>
Hello world
</Button>
</p>
<p>
<Button disabled onClick={action('onClick')} variant={variant}>
Hello world
</Button>
</p>
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
<React.Fragment key={size}>
{[
ButtonVariant.Primary,
ButtonVariant.Secondary,
ButtonVariant.SecondaryAffirmative,
ButtonVariant.SecondaryDestructive,
ButtonVariant.Destructive,
].map(variant => (
<React.Fragment key={variant}>
<p>
<Button onClick={action('onClick')} size={size} variant={variant}>
Hello world
</Button>
</p>
<p>
<Button
disabled
onClick={action('onClick')}
size={size}
variant={variant}
>
Hello world
</Button>
</p>
</React.Fragment>
))}
</React.Fragment>
))}
</>

View file

@ -6,6 +6,11 @@ import classNames from 'classnames';
import { assert } from '../util/assert';
export enum ButtonSize {
Medium,
Small,
}
export enum ButtonVariant {
Primary,
Secondary,
@ -17,6 +22,7 @@ export enum ButtonVariant {
type PropsType = {
className?: string;
disabled?: boolean;
size?: ButtonSize;
variant?: ButtonVariant;
} & (
| {
@ -41,6 +47,11 @@ type PropsType = {
}
);
const SIZE_CLASS_NAMES = new Map<ButtonSize, string>([
[ButtonSize.Medium, 'module-Button--medium'],
[ButtonSize.Small, 'module-Button--small'],
]);
const VARIANT_CLASS_NAMES = new Map<ButtonVariant, string>([
[ButtonVariant.Primary, 'module-Button--primary'],
[ButtonVariant.Secondary, 'module-Button--secondary'],
@ -61,6 +72,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
children,
className,
disabled = false,
size = ButtonSize.Medium,
variant = ButtonVariant.Primary,
} = props;
const ariaLabel = props['aria-label'];
@ -75,13 +87,21 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
({ type } = props);
}
const sizeClassName = SIZE_CLASS_NAMES.get(size);
assert(sizeClassName, '<Button> size not found');
const variantClassName = VARIANT_CLASS_NAMES.get(variant);
assert(variantClassName, '<Button> variant not found');
return (
<button
aria-label={ariaLabel}
className={classNames('module-Button', variantClassName, className)}
className={classNames(
'module-Button',
sizeClassName,
variantClassName,
className
)}
disabled={disabled}
onClick={onClick}
ref={ref}

View file

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -20,12 +20,15 @@ const getAvatarPath = () =>
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
const updateSharedGroups = action('updateSharedGroups');
storiesOf('Components/Conversation/ConversationHero', module)
.add('Direct (Three Other Groups)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()}
@ -33,6 +36,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
unblurAvatar={action('unblurAvatar')}
/>
@ -44,6 +48,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()}
@ -51,6 +56,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
/>
@ -62,6 +68,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()}
@ -69,6 +76,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers']}
unblurAvatar={action('unblurAvatar')}
/>
@ -80,6 +88,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()}
@ -87,6 +96,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
@ -98,6 +108,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={text('title', 'Cayce Bollard (profile)')}
avatarPath={getAvatarPath()}
@ -105,6 +116,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
@ -116,6 +128,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}>
<ConversationHero
about={getAbout()}
acceptedMessageRequest
i18n={i18n}
title={text('title', '+1 (646) 327-2700')}
avatarPath={getAvatarPath()}
@ -123,6 +136,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
@ -135,6 +149,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<ConversationHero
i18n={i18n}
title={text('title', 'Unknown contact')}
acceptedMessageRequest
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
@ -142,6 +157,26 @@ storiesOf('Components/Conversation/ConversationHero', module)
conversationType="direct"
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
})
.add('Direct (No Groups, No Data, Not Accepted)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
i18n={i18n}
title={text('title', 'Unknown contact')}
acceptedMessageRequest={false}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={text('phoneNumber', '')}
conversationType="direct"
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
@ -150,12 +185,14 @@ storiesOf('Components/Conversation/ConversationHero', module)
return (
<div style={{ width: '480px' }}>
<ConversationHero
acceptedMessageRequest
i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
membersCount={numberKnob('membersCount', 22)}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
@ -164,12 +201,14 @@ storiesOf('Components/Conversation/ConversationHero', module)
return (
<div style={{ width: '480px' }}>
<ConversationHero
acceptedMessageRequest
i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
membersCount={1}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
@ -178,12 +217,14 @@ storiesOf('Components/Conversation/ConversationHero', module)
return (
<div style={{ width: '480px' }}>
<ConversationHero
acceptedMessageRequest
i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
membersCount={0}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
@ -192,12 +233,14 @@ storiesOf('Components/Conversation/ConversationHero', module)
return (
<div style={{ width: '480px' }}>
<ConversationHero
acceptedMessageRequest
i18n={i18n}
title={text('title', 'Unknown group')}
name={text('groupName', '')}
conversationType="group"
membersCount={0}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
@ -212,6 +255,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
conversationType="direct"
phoneNumber={getPhoneNumber()}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);

View file

@ -1,12 +1,16 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import Measure from 'react-measure';
import { Avatar, AvatarBlur, Props as AvatarProps } from '../Avatar';
import { ContactName } from './ContactName';
import { About } from './About';
import { SharedGroupNames } from '../SharedGroupNames';
import { LocalizerType } from '../../types/Util';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { assert } from '../../util/assert';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
export type Props = {
@ -14,25 +18,34 @@ export type Props = {
acceptedMessageRequest?: boolean;
i18n: LocalizerType;
isMe?: boolean;
sharedGroupNames?: Array<string>;
membersCount?: number;
phoneNumber?: string;
onHeightChange?: () => unknown;
phoneNumber?: string;
sharedGroupNames?: Array<string>;
unblurAvatar: () => void;
unblurredAvatarPath?: string;
updateSharedGroups?: () => unknown;
updateSharedGroups: () => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
const renderMembershipRow = ({
i18n,
phoneNumber,
sharedGroupNames = [],
acceptedMessageRequest,
conversationType,
i18n,
isMe,
onClickMessageRequestWarning,
phoneNumber,
sharedGroupNames,
}: Pick<
Props,
'i18n' | 'phoneNumber' | 'sharedGroupNames' | 'conversationType' | 'isMe'
>) => {
| 'acceptedMessageRequest'
| 'conversationType'
| 'i18n'
| 'isMe'
| 'phoneNumber'
> &
Required<Pick<Props, 'sharedGroupNames'>> & {
onClickMessageRequestWarning: () => void;
}) => {
const className = 'module-conversation-hero__membership';
if (conversationType !== 'direct') {
@ -54,12 +67,27 @@ const renderMembershipRow = ({
</div>
);
}
if (!phoneNumber) {
if (acceptedMessageRequest) {
if (phoneNumber) {
return null;
}
return <div className={className}>{i18n('no-groups-in-common')}</div>;
}
return null;
return (
<div className="module-conversation-hero__message-request-warning">
<div className="module-conversation-hero__message-request-warning__message">
{i18n('no-groups-in-common-warning')}
</div>
<Button
onClick={onClickMessageRequestWarning}
size={ButtonSize.Small}
variant={ButtonVariant.SecondaryAffirmative}
>
{i18n('MessageRequestWarning__learn-more')}
</Button>
</div>
);
};
export const ConversationHero = ({
@ -81,37 +109,31 @@ export const ConversationHero = ({
unblurredAvatarPath,
updateSharedGroups,
}: Props): JSX.Element => {
const firstRenderRef = React.useRef(true);
const firstRenderRef = useRef(true);
// TODO: DESKTOP-686
/* eslint-disable react-hooks/exhaustive-deps */
React.useEffect(() => {
// If any of the depenencies for this hook change then the height of this
// component may have changed. The cleanup function notifies listeners of
// any potential height changes.
return () => {
// Kick off the expensive hydration of the current sharedGroupNames
if (updateSharedGroups) {
updateSharedGroups();
}
const [height, setHeight] = useState<undefined | number>();
const [
isShowingMessageRequestWarning,
setIsShowingMessageRequestWarning,
] = useState(false);
const closeMessageRequestWarning = () => {
setIsShowingMessageRequestWarning(false);
};
if (onHeightChange && !firstRenderRef.current) {
onHeightChange();
} else {
firstRenderRef.current = false;
}
};
}, [
firstRenderRef,
onHeightChange,
// Avoid collisions in these dependencies by prefixing them
// These dependencies may be dynamic, and therefore may cause height changes
`mc-${membersCount}`,
`n-${name}`,
`pn-${profileName}`,
sharedGroupNames.map(g => `g-${g}`).join(' '),
]);
/* eslint-enable react-hooks/exhaustive-deps */
useEffect(() => {
// Kick off the expensive hydration of the current sharedGroupNames
updateSharedGroups();
}, [updateSharedGroups]);
useEffect(() => {
firstRenderRef.current = false;
}, []);
useEffect(() => {
if (!firstRenderRef.current && onHeightChange) {
onHeightChange();
}
}, [height, onHeightChange]);
let avatarBlur: AvatarBlur;
let avatarOnClick: undefined | (() => void);
@ -136,58 +158,92 @@ export const ConversationHero = ({
/* eslint-disable no-nested-ternary */
return (
<div className="module-conversation-hero">
<Avatar
i18n={i18n}
blur={avatarBlur}
color={color}
noteToSelf={isMe}
avatarPath={avatarPath}
conversationType={conversationType}
name={name}
onClick={avatarOnClick}
profileName={profileName}
title={title}
size={112}
className="module-conversation-hero__avatar"
/>
<h1 className="module-conversation-hero__profile-name">
{isMe ? (
i18n('noteToSelf')
) : (
<ContactName
title={title}
name={name}
profileName={profileName}
phoneNumber={phoneNumber}
i18n={i18n}
/>
<>
<Measure
bounds
onResize={({ bounds }) => {
assert(bounds, 'We should be measuring the bounds');
setHeight(bounds.height);
}}
>
{({ measureRef }) => (
<div className="module-conversation-hero" ref={measureRef}>
<Avatar
i18n={i18n}
blur={avatarBlur}
color={color}
noteToSelf={isMe}
avatarPath={avatarPath}
conversationType={conversationType}
name={name}
onClick={avatarOnClick}
profileName={profileName}
title={title}
size={112}
className="module-conversation-hero__avatar"
/>
<h1 className="module-conversation-hero__profile-name">
{isMe ? (
i18n('noteToSelf')
) : (
<ContactName
title={title}
name={name}
profileName={profileName}
phoneNumber={phoneNumber}
i18n={i18n}
/>
)}
</h1>
{about && !isMe && (
<div className="module-about__container">
<About text={about} />
</div>
)}
{!isMe ? (
<div className="module-conversation-hero__with">
{membersCount === 1
? i18n('ConversationHero--members-1')
: membersCount !== undefined
? i18n('ConversationHero--members', [`${membersCount}`])
: phoneNumberOnly
? null
: phoneNumber}
</div>
) : null}
{renderMembershipRow({
acceptedMessageRequest,
conversationType,
i18n,
isMe,
onClickMessageRequestWarning() {
setIsShowingMessageRequestWarning(true);
},
phoneNumber,
sharedGroupNames,
})}
</div>
)}
</h1>
{about && !isMe && (
<div className="module-about__container">
<About text={about} />
</div>
</Measure>
{isShowingMessageRequestWarning && (
<ConfirmationDialog
i18n={i18n}
onClose={closeMessageRequestWarning}
actions={[
{
text: i18n('MessageRequestWarning__dialog__learn-even-more'),
action: () => {
window.location.href =
'https://support.signal.org/hc/articles/360007459591';
closeMessageRequestWarning();
},
},
]}
>
{i18n('MessageRequestWarning__dialog__details')}
</ConfirmationDialog>
)}
{!isMe ? (
<div className="module-conversation-hero__with">
{membersCount === 1
? i18n('ConversationHero--members-1')
: membersCount !== undefined
? i18n('ConversationHero--members', [`${membersCount}`])
: phoneNumberOnly
? null
: phoneNumber}
</div>
) : null}
{renderMembershipRow({
conversationType,
i18n,
isMe,
phoneNumber,
sharedGroupNames,
})}
</div>
</>
);
/* eslint-enable no-nested-ternary */
};

View file

@ -315,6 +315,7 @@ const renderHeroRow = () => (
conversationType="direct"
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={noop}
/>
);
const renderLoadingRow = () => <TimelineLoadingRow state="loading" />;

View file

@ -16509,8 +16509,8 @@
{
"rule": "React-useRef",
"path": "ts/components/conversation/ConversationHero.js",
"line": " const firstRenderRef = React.useRef(true);",
"lineNumber": 49,
"line": " const firstRenderRef = react_1.useRef(true);",
"lineNumber": 61,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
@ -16518,8 +16518,8 @@
{
"rule": "React-useRef",
"path": "ts/components/conversation/ConversationHero.tsx",
"line": " const firstRenderRef = React.useRef(true);",
"lineNumber": 84,
"line": " const firstRenderRef = useRef(true);",
"lineNumber": 112,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."