Blur avatars of unapproved conversations

This commit is contained in:
Evan Hahn 2021-04-30 14:40:25 -05:00 committed by GitHub
parent bbd7fd3854
commit 05703c2719
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 474 additions and 124 deletions

View file

@ -2,6 +2,7 @@
// 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 { isBoolean } from 'lodash';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select, text } from '@storybook/addon-knobs'; import { boolean, select, text } from '@storybook/addon-knobs';
@ -30,6 +31,9 @@ const conversationTypeMap: Record<string, Props['conversationType']> = {
}; };
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest)
? overrideProps.acceptedMessageRequest
: true,
avatarPath: text('avatarPath', overrideProps.avatarPath || ''), avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
blur: overrideProps.blur, blur: overrideProps.blur,
color: select('color', colorMap, overrideProps.color || 'blue'), color: select('color', colorMap, overrideProps.color || 'blue'),
@ -151,7 +155,16 @@ story.add('Loading', () => {
return sizes.map(size => <Avatar key={size} {...props} size={size} />); return sizes.map(size => <Avatar key={size} {...props} size={size} />);
}); });
story.add('Blurred', () => { story.add('Blurred based on props', () => {
const props = createProps({
acceptedMessageRequest: false,
avatarPath: '/fixtures/kitten-3-64-64.jpg',
});
return sizes.map(size => <Avatar key={size} {...props} size={size} />);
});
story.add('Force-blurred', () => {
const props = createProps({ const props = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarPath: '/fixtures/kitten-3-64-64.jpg',
blur: AvatarBlur.BlurPicture, blur: AvatarBlur.BlurPicture,

View file

@ -17,6 +17,7 @@ import { LocalizerType } from '../types/Util';
import { ColorType } from '../types/Colors'; import { ColorType } from '../types/Colors';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
export enum AvatarBlur { export enum AvatarBlur {
NoBlur, NoBlur,
@ -39,13 +40,17 @@ export type Props = {
color?: ColorType; color?: ColorType;
loading?: boolean; loading?: boolean;
acceptedMessageRequest?: boolean;
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
noteToSelf?: boolean; isMe?: boolean;
title: string;
name?: string; name?: string;
noteToSelf?: boolean;
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
sharedGroupNames?: Array<string>;
size: AvatarSize; size: AvatarSize;
title: string;
unblurredAvatarPath?: string;
onClick?: () => unknown; onClick?: () => unknown;
@ -55,19 +60,34 @@ export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
} & Pick<React.HTMLProps<HTMLDivElement>, 'className'>; } & Pick<React.HTMLProps<HTMLDivElement>, 'className'>;
const getDefaultBlur = (
...args: Parameters<typeof shouldBlurAvatar>
): AvatarBlur =>
shouldBlurAvatar(...args) ? AvatarBlur.BlurPicture : AvatarBlur.NoBlur;
export const Avatar: FunctionComponent<Props> = ({ export const Avatar: FunctionComponent<Props> = ({
acceptedMessageRequest,
avatarPath, avatarPath,
className, className,
color, color,
conversationType, conversationType,
i18n, i18n,
isMe,
innerRef, innerRef,
loading, loading,
noteToSelf, noteToSelf,
onClick, onClick,
sharedGroupNames,
size, size,
title, title,
blur = AvatarBlur.NoBlur, unblurredAvatarPath,
blur = getDefaultBlur({
acceptedMessageRequest,
avatarPath,
isMe,
sharedGroupNames,
unblurredAvatarPath,
}),
}) => { }) => {
const [imageBroken, setImageBroken] = useState(false); const [imageBroken, setImageBroken] = useState(false);
@ -111,6 +131,7 @@ export const Avatar: FunctionComponent<Props> = ({
); );
} else if (hasImage) { } else if (hasImage) {
assert(avatarPath, 'avatarPath should be defined here'); assert(avatarPath, 'avatarPath should be defined here');
assert( assert(
blur !== AvatarBlur.BlurPictureWithClickToView || size >= 100, blur !== AvatarBlur.BlurPictureWithClickToView || size >= 100,
'Rendering "click to view" for a small avatar. This may not render correctly' 'Rendering "click to view" for a small avatar. This may not render correctly'

View file

@ -6,17 +6,21 @@ import { LocalizerType } from '../types/Util';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import { Intl } from './Intl'; import { Intl } from './Intl';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { ColorType } from '../types/Colors'; import { ConversationType } from '../state/ducks/conversations';
type Props = { type Props = {
conversation: { conversation: Pick<
avatarPath?: string; ConversationType,
color?: ColorType; | 'acceptedMessageRequest'
name?: string; | 'avatarPath'
phoneNumber?: string; | 'color'
profileName?: string; | 'name'
title: string; | 'phoneNumber'
}; | 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
i18n: LocalizerType; i18n: LocalizerType;
close: () => void; close: () => void;
}; };
@ -39,6 +43,7 @@ export const CallNeedPermissionScreen: React.FC<Props> = ({
return ( return (
<div className="module-call-need-permission-screen"> <div className="module-call-need-permission-screen">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarPath={conversation.avatarPath} avatarPath={conversation.avatarPath}
color={conversation.color || 'ultramarine'} color={conversation.color || 'ultramarine'}
noteToSelf={false} noteToSelf={false}

View file

@ -10,36 +10,45 @@ import { Emojify } from './conversation/Emojify';
import { InContactsIcon } from './InContactsIcon'; import { InContactsIcon } from './InContactsIcon';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { ColorType } from '../types/Colors'; import { ConversationType } from '../state/ducks/conversations';
type Props = { type Props = {
about?: string;
avatarPath?: string;
color?: ColorType;
i18n: LocalizerType; i18n: LocalizerType;
isAdmin?: boolean; isAdmin?: boolean;
isMe?: boolean;
name?: string;
onClick?: () => void; onClick?: () => void;
phoneNumber?: string; } & Pick<
profileName?: string; ConversationType,
title: string; | 'about'
}; | 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
export class ContactListItem extends React.Component<Props> { export class ContactListItem extends React.Component<Props> {
public renderAvatar(): JSX.Element { public renderAvatar(): JSX.Element {
const { const {
acceptedMessageRequest,
avatarPath, avatarPath,
i18n,
color, color,
i18n,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
title, title,
unblurredAvatarPath,
} = this.props; } = this.props;
return ( return (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
conversationType="direct" conversationType="direct"
@ -48,7 +57,9 @@ export class ContactListItem extends React.Component<Props> {
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames}
size={52} size={52}
unblurredAvatarPath={unblurredAvatarPath}
/> />
); );
} }

View file

@ -3,26 +3,33 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { ColorType } from '../types/Colors'; import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
export type PropsType = { export type PropsType = {
avatarPath?: string;
color?: ColorType;
firstName?: string;
i18n: LocalizerType; i18n: LocalizerType;
id: string;
isMe?: boolean;
name?: string;
onClickRemove: (id: string) => void; onClickRemove: (id: string) => void;
phoneNumber?: string; } & Pick<
profileName?: string; ConversationType,
title: string; | 'about'
}; | 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'firstName'
| 'id'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
export const ContactPill: FunctionComponent<PropsType> = ({ export const ContactPill: FunctionComponent<PropsType> = ({
acceptedMessageRequest,
avatarPath, avatarPath,
color, color,
firstName, firstName,
@ -31,7 +38,9 @@ export const ContactPill: FunctionComponent<PropsType> = ({
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
title, title,
unblurredAvatarPath,
onClickRemove, onClickRemove,
}) => { }) => {
const removeLabel = i18n('ContactPill--remove'); const removeLabel = i18n('ContactPill--remove');
@ -39,6 +48,7 @@ export const ContactPill: FunctionComponent<PropsType> = ({
return ( return (
<div className="module-ContactPill"> <div className="module-ContactPill">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
noteToSelf={false} noteToSelf={false}
@ -48,7 +58,9 @@ export const ContactPill: FunctionComponent<PropsType> = ({
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
unblurredAvatarPath={unblurredAvatarPath}
/> />
<ContactName <ContactName
firstName={firstName} firstName={firstName}

View file

@ -86,6 +86,7 @@ export const SafetyNumberChangeDialog = ({
key={contact.id} key={contact.id}
> >
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarPath={contact.avatarPath}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
@ -94,7 +95,9 @@ export const SafetyNumberChangeDialog = ({
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
profileName={contact.profileName} profileName={contact.profileName}
title={contact.title} title={contact.title}
sharedGroupNames={contact.sharedGroupNames}
size={52} size={52}
unblurredAvatarPath={contact.unblurredAvatarPath}
/> />
<div className="module-SafetyNumberChangeDialog__contact--wrapper"> <div className="module-SafetyNumberChangeDialog__contact--wrapper">
<div className="module-SafetyNumberChangeDialog__contact--name"> <div className="module-SafetyNumberChangeDialog__contact--name">

View file

@ -1,4 +1,4 @@
// 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, { ReactPortal } from 'react'; import React, { ReactPortal } from 'react';
@ -106,14 +106,17 @@ export const ContactModal = ({
aria-label={i18n('close')} aria-label={i18n('close')}
/> />
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarPath={contact.avatarPath}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
name={contact.name} name={contact.name}
profileName={contact.profileName} profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames}
size={96} size={96}
title={contact.title} title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath}
/> />
<div className="module-contact-modal__name">{contact.title}</div> <div className="module-contact-modal__name">{contact.title}</div>
<div className="module-about__container"> <div className="module-about__container">

View file

@ -17,7 +17,7 @@ import { Avatar, AvatarSize } from '../Avatar';
import { InContactsIcon } from '../InContactsIcon'; import { InContactsIcon } from '../InContactsIcon';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors'; import { ConversationType } from '../../state/ducks/conversations';
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions'; import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
import { import {
ExpirationTimerOptions, ExpirationTimerOptions,
@ -35,33 +35,33 @@ export enum OutgoingCallButtonStyle {
export type PropsDataType = { export type PropsDataType = {
conversationTitle?: string; conversationTitle?: string;
id: string;
name?: string;
phoneNumber?: string;
profileName?: string;
color?: ColorType;
avatarPath?: string;
type: 'direct' | 'group';
title: string;
acceptedMessageRequest?: boolean;
isVerified?: boolean;
isMe?: boolean;
isArchived?: boolean;
isPinned?: boolean;
isMissingMandatoryProfileSharing?: boolean; isMissingMandatoryProfileSharing?: boolean;
left?: boolean;
markedUnread?: boolean;
groupVersion?: number;
canChangeTimer?: boolean;
expireTimer?: number;
muteExpiresAt?: number;
showBackButton?: boolean;
outgoingCallButtonStyle: OutgoingCallButtonStyle; outgoingCallButtonStyle: OutgoingCallButtonStyle;
}; showBackButton?: boolean;
} & Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'canChangeTimer'
| 'color'
| 'expireTimer'
| 'groupVersion'
| 'id'
| 'isArchived'
| 'isMe'
| 'isPinned'
| 'isVerified'
| 'left'
| 'markedUnread'
| 'muteExpiresAt'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
>;
export type PropsActionsType = { export type PropsActionsType = {
onSetMuteNotifications: (seconds: number) => void; onSetMuteNotifications: (seconds: number) => void;
@ -180,6 +180,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
private renderAvatar(): ReactNode { private renderAvatar(): ReactNode {
const { const {
acceptedMessageRequest,
avatarPath, avatarPath,
color, color,
i18n, i18n,
@ -188,22 +189,28 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
title, title,
unblurredAvatarPath,
} = this.props; } = this.props;
return ( return (
<span className="module-ConversationHeader__header__avatar"> <span className="module-ConversationHeader__header__avatar">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
conversationType={type} conversationType={type}
i18n={i18n} i18n={i18n}
isMe={isMe}
noteToSelf={isMe} noteToSelf={isMe}
title={title} title={title}
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
/> />
</span> </span>
); );

View file

@ -4,6 +4,7 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number as numberKnob, text } from '@storybook/addon-knobs'; import { number as numberKnob, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { ConversationHero } from './ConversationHero'; import { ConversationHero } from './ConversationHero';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
@ -33,6 +34,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']} sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -50,6 +52,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']} sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -67,6 +70,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={['NYC Rock Climbers']} sharedGroupNames={['NYC Rock Climbers']}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -84,6 +88,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={[]} sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -101,6 +106,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={[]} sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -118,6 +124,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={[]} sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -134,6 +141,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
phoneNumber={text('phoneNumber', '')} phoneNumber={text('phoneNumber', '')}
conversationType="direct" conversationType="direct"
sharedGroupNames={[]} sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -147,6 +155,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
conversationType="group" conversationType="group"
membersCount={numberKnob('membersCount', 22)} membersCount={numberKnob('membersCount', 22)}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -160,6 +169,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
conversationType="group" conversationType="group"
membersCount={1} membersCount={1}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -173,6 +183,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
conversationType="group" conversationType="group"
membersCount={0} membersCount={0}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -186,6 +197,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
name={text('groupName', '')} name={text('groupName', '')}
conversationType="group" conversationType="group"
membersCount={0} membersCount={0}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );
@ -199,6 +211,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
title={getTitle()} title={getTitle()}
conversationType="direct" conversationType="direct"
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
unblurAvatar={action('unblurAvatar')}
/> />
</div> </div>
); );

View file

@ -1,21 +1,25 @@
// 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 { Avatar, Props as AvatarProps } from '../Avatar'; import { Avatar, AvatarBlur, Props as AvatarProps } from '../Avatar';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { About } from './About'; import { About } from './About';
import { SharedGroupNames } from '../SharedGroupNames'; import { SharedGroupNames } from '../SharedGroupNames';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
export type Props = { export type Props = {
about?: string; about?: string;
acceptedMessageRequest?: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isMe?: boolean; isMe?: boolean;
sharedGroupNames?: Array<string>; sharedGroupNames?: Array<string>;
membersCount?: number; membersCount?: number;
phoneNumber?: string; phoneNumber?: string;
onHeightChange?: () => unknown; onHeightChange?: () => unknown;
unblurAvatar: () => void;
unblurredAvatarPath?: string;
updateSharedGroups?: () => unknown; updateSharedGroups?: () => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>; } & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
@ -61,6 +65,7 @@ const renderMembershipRow = ({
export const ConversationHero = ({ export const ConversationHero = ({
i18n, i18n,
about, about,
acceptedMessageRequest,
avatarPath, avatarPath,
color, color,
conversationType, conversationType,
@ -72,6 +77,8 @@ export const ConversationHero = ({
profileName, profileName,
title, title,
onHeightChange, onHeightChange,
unblurAvatar,
unblurredAvatarPath,
updateSharedGroups, updateSharedGroups,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const firstRenderRef = React.useRef(true); const firstRenderRef = React.useRef(true);
@ -106,6 +113,23 @@ export const ConversationHero = ({
]); ]);
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
let avatarBlur: AvatarBlur;
let avatarOnClick: undefined | (() => void);
if (
shouldBlurAvatar({
acceptedMessageRequest,
avatarPath,
isMe,
sharedGroupNames,
unblurredAvatarPath,
})
) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = unblurAvatar;
} else {
avatarBlur = AvatarBlur.NoBlur;
}
const phoneNumberOnly = Boolean( const phoneNumberOnly = Boolean(
!name && !profileName && conversationType === 'direct' !name && !profileName && conversationType === 'direct'
); );
@ -115,11 +139,13 @@ export const ConversationHero = ({
<div className="module-conversation-hero"> <div className="module-conversation-hero">
<Avatar <Avatar
i18n={i18n} i18n={i18n}
blur={avatarBlur}
color={color} color={color}
noteToSelf={isMe} noteToSelf={isMe}
avatarPath={avatarPath} avatarPath={avatarPath}
conversationType={conversationType} conversationType={conversationType}
name={name} name={name}
onClick={avatarOnClick}
profileName={profileName} profileName={profileName}
title={title} title={title}
size={112} size={112}

View file

@ -111,13 +111,17 @@ export type PropsData = {
contact?: ContactType; contact?: ContactType;
author: Pick< author: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath' | 'avatarPath'
| 'color' | 'color'
| 'id' | 'id'
| 'isMe'
| 'name' | 'name'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
| 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath'
>; >;
reducedMotion?: boolean; reducedMotion?: boolean;
conversationType: ConversationTypesType; conversationType: ConversationTypesType;
@ -1159,15 +1163,19 @@ export class Message extends React.Component<Props, State> {
tabIndex={0} tabIndex={0}
> >
<Avatar <Avatar
acceptedMessageRequest={author.acceptedMessageRequest}
avatarPath={author.avatarPath} avatarPath={author.avatarPath}
color={author.color} color={author.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={author.isMe}
name={author.name} name={author.name}
phoneNumber={author.phoneNumber} phoneNumber={author.phoneNumber}
profileName={author.profileName} profileName={author.profileName}
title={author.title} sharedGroupNames={author.sharedGroupNames}
size={28} size={28}
title={author.title}
unblurredAvatarPath={author.unblurredAvatarPath}
/> />
</button> </button>
</div> </div>

View file

@ -271,6 +271,8 @@ const actions = () => ({
onBlockAndDelete: action('onBlockAndDelete'), onBlockAndDelete: action('onBlockAndDelete'),
onDelete: action('onDelete'), onDelete: action('onDelete'),
onUnblock: action('onUnblock'), onUnblock: action('onUnblock'),
unblurAvatar: action('unblurAvatar'),
}); });
const renderItem = (id: string) => ( const renderItem = (id: string) => (
@ -312,6 +314,7 @@ const renderHeroRow = () => (
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']} sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
/> />
); );
const renderLoadingRow = () => <TimelineLoadingRow state="loading" />; const renderLoadingRow = () => <TimelineLoadingRow state="loading" />;

View file

@ -81,6 +81,7 @@ type PropsHousekeepingType = {
renderHeroRow: ( renderHeroRow: (
id: string, id: string,
resizeHeroRow: () => unknown, resizeHeroRow: () => unknown,
unblurAvatar: () => void,
updateSharedGroups: () => unknown updateSharedGroups: () => unknown
) => JSX.Element; ) => JSX.Element;
renderLoadingRow: (id: string) => JSX.Element; renderLoadingRow: (id: string) => JSX.Element;
@ -113,6 +114,7 @@ type PropsActionsType = {
onUnblock: () => unknown; onUnblock: () => unknown;
selectMessage: (messageId: string, conversationId: string) => unknown; selectMessage: (messageId: string, conversationId: string) => unknown;
clearSelectedMessage: () => unknown; clearSelectedMessage: () => unknown;
unblurAvatar: () => void;
updateSharedGroups: () => unknown; updateSharedGroups: () => unknown;
} & MessageActionsType & } & MessageActionsType &
SafetyNumberActionsType; SafetyNumberActionsType;
@ -583,6 +585,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
renderLoadingRow, renderLoadingRow,
renderLastSeenIndicator, renderLastSeenIndicator,
renderTypingBubble, renderTypingBubble,
unblurAvatar,
updateSharedGroups, updateSharedGroups,
} = this.props; } = this.props;
const { lastMeasuredWarningHeight } = this.state; const { lastMeasuredWarningHeight } = this.state;
@ -602,7 +605,12 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
{this.getWarning() ? ( {this.getWarning() ? (
<div style={{ height: lastMeasuredWarningHeight }} /> <div style={{ height: lastMeasuredWarningHeight }} />
) : null} ) : null}
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)} {renderHeroRow(
id,
this.resizeHeroRow,
unblurAvatar,
updateSharedGroups
)}
</div> </div>
); );
} else if (!haveOldest && row === 0) { } else if (!haveOldest && row === 0) {

View file

@ -9,8 +9,8 @@ import { Avatar, AvatarSize } from '../Avatar';
import { Timestamp } from '../conversation/Timestamp'; import { Timestamp } from '../conversation/Timestamp';
import { isConversationUnread } from '../../util/isConversationUnread'; import { isConversationUnread } from '../../util/isConversationUnread';
import { cleanId } from '../_util'; import { cleanId } from '../_util';
import { ColorType } from '../../types/Colors';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations';
const BASE_CLASS_NAME = const BASE_CLASS_NAME =
'module-conversation-list__item--contact-or-conversation'; 'module-conversation-list__item--contact-or-conversation';
@ -23,33 +23,40 @@ export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`; const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
type PropsType = { type PropsType = {
avatarPath?: string;
checked?: boolean; checked?: boolean;
color?: ColorType;
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
disabled?: boolean; disabled?: boolean;
headerDate?: number; headerDate?: number;
headerName: ReactNode; headerName: ReactNode;
i18n: LocalizerType;
id?: string; id?: string;
isMe?: boolean; i18n: LocalizerType;
isNoteToSelf?: boolean; isNoteToSelf?: boolean;
isSelected: boolean; isSelected: boolean;
markedUnread?: boolean; markedUnread?: boolean;
messageId?: string; messageId?: string;
messageStatusIcon?: ReactNode; messageStatusIcon?: ReactNode;
messageText?: ReactNode; messageText?: ReactNode;
name?: string;
onClick?: () => void; onClick?: () => void;
phoneNumber?: string;
profileName?: string;
style: CSSProperties; style: CSSProperties;
title: string;
unreadCount?: number; unreadCount?: number;
}; } & Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'markedUnread'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo( export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
({ ({
acceptedMessageRequest,
avatarPath, avatarPath,
checked, checked,
color, color,
@ -69,8 +76,10 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
onClick, onClick,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
style, style,
title, title,
unblurredAvatarPath,
unreadCount, unreadCount,
}) => { }) => {
const isUnread = isConversationUnread({ markedUnread, unreadCount }); const isUnread = isConversationUnread({ markedUnread, unreadCount });
@ -112,6 +121,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
<> <>
<div className={`${BASE_CLASS_NAME}__avatar-container`}> <div className={`${BASE_CLASS_NAME}__avatar-container`}>
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
noteToSelf={isAvatarNoteToSelf} noteToSelf={isAvatarNoteToSelf}
@ -121,7 +131,9 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.FIFTY_TWO} size={AvatarSize.FIFTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
/> />
{isUnread && ( {isUnread && (
<div className={`${BASE_CLASS_NAME}__unread-count`}> <div className={`${BASE_CLASS_NAME}__unread-count`}>

View file

@ -4,7 +4,7 @@
import React, { CSSProperties, FunctionComponent, ReactNode } from 'react'; import React, { CSSProperties, FunctionComponent, ReactNode } from 'react';
import { BaseConversationListItem } from './BaseConversationListItem'; import { BaseConversationListItem } from './BaseConversationListItem';
import { ColorType } from '../../types/Colors'; import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About'; import { About } from '../conversation/About';
@ -17,18 +17,23 @@ export enum ContactCheckboxDisabledReason {
} }
export type PropsDataType = { export type PropsDataType = {
about?: string;
avatarPath?: string;
color?: ColorType;
disabledReason?: ContactCheckboxDisabledReason; disabledReason?: ContactCheckboxDisabledReason;
id: string;
isMe?: boolean;
isChecked: boolean; isChecked: boolean;
name?: string; } & Pick<
phoneNumber?: string; ConversationType,
profileName?: string; | 'about'
title: string; | 'acceptedMessageRequest'
}; | 'avatarPath'
| 'color'
| 'id'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
type PropsHousekeepingType = { type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
@ -44,6 +49,7 @@ type PropsType = PropsDataType & PropsHousekeepingType;
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo( export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
({ ({
about, about,
acceptedMessageRequest,
avatarPath, avatarPath,
color, color,
disabledReason, disabledReason,
@ -55,8 +61,10 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
onClick, onClick,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
style, style,
title, title,
unblurredAvatarPath,
}) => { }) => {
const disabled = Boolean(disabledReason); const disabled = Boolean(disabledReason);
@ -87,6 +95,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
checked={isChecked} checked={isChecked}
color={color} color={color}
@ -102,8 +111,10 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
onClick={onClickItem} onClick={onClickItem}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames}
style={style} style={style}
title={title} title={title}
unblurredAvatarPath={unblurredAvatarPath}
/> />
); );
} }

View file

@ -4,23 +4,27 @@
import React, { CSSProperties, FunctionComponent } from 'react'; import React, { CSSProperties, FunctionComponent } from 'react';
import { BaseConversationListItem } from './BaseConversationListItem'; import { BaseConversationListItem } from './BaseConversationListItem';
import { ColorType } from '../../types/Colors'; import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About'; import { About } from '../conversation/About';
export type PropsDataType = { export type PropsDataType = Pick<
about?: string; ConversationType,
avatarPath?: string; | 'about'
color?: ColorType; | 'acceptedMessageRequest'
id: string; | 'avatarPath'
isMe?: boolean; | 'color'
name?: string; | 'id'
phoneNumber?: string; | 'isMe'
profileName?: string; | 'name'
title: string; | 'phoneNumber'
type: 'group' | 'direct'; | 'profileName'
}; | 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
>;
type PropsHousekeepingType = { type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
@ -33,6 +37,7 @@ type PropsType = PropsDataType & PropsHousekeepingType;
export const ContactListItem: FunctionComponent<PropsType> = React.memo( export const ContactListItem: FunctionComponent<PropsType> = React.memo(
({ ({
about, about,
acceptedMessageRequest,
avatarPath, avatarPath,
color, color,
i18n, i18n,
@ -42,9 +47,11 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
onClick, onClick,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
style, style,
title, title,
type, type,
unblurredAvatarPath,
}) => { }) => {
const headerName = isMe ? ( const headerName = isMe ? (
i18n('noteToSelf') i18n('noteToSelf')
@ -63,6 +70,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
conversationType={type} conversationType={type}
@ -76,8 +84,10 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
onClick={onClick ? () => onClick(id) : undefined} onClick={onClick ? () => onClick(id) : undefined}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames}
style={style} style={style}
title={title} title={title}
unblurredAvatarPath={unblurredAvatarPath}
/> />
); );
} }

View file

@ -44,6 +44,8 @@ export type PropsData = {
avatarPath?: string; avatarPath?: string;
isMe?: boolean; isMe?: boolean;
muteExpiresAt?: number; muteExpiresAt?: number;
sharedGroupNames?: Array<string>;
unblurredAvatarPath?: string;
lastUpdated?: number; lastUpdated?: number;
unreadCount?: number; unreadCount?: number;
@ -89,11 +91,13 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
onClick, onClick,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames,
shouldShowDraft, shouldShowDraft,
style, style,
title, title,
type, type,
typingContact, typingContact,
unblurredAvatarPath,
unreadCount, unreadCount,
}) => { }) => {
const headerName = isMe ? ( const headerName = isMe ? (
@ -180,6 +184,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
conversationType={type} conversationType={type}
@ -196,9 +201,11 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
onClick={onClickItem} onClick={onClickItem}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames}
style={style} style={style}
title={title} title={title}
unreadCount={unreadCount} unreadCount={unreadCount}
unblurredAvatarPath={unblurredAvatarPath}
/> />
); );
} }

View file

@ -1,4 +1,4 @@
// 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, { import React, {
@ -14,8 +14,8 @@ import { ContactName } from '../conversation/ContactName';
import { assert } from '../../util/assert'; import { assert } from '../../util/assert';
import { BodyRangesType, LocalizerType } from '../../types/Util'; import { BodyRangesType, LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors';
import { BaseConversationListItem } from './BaseConversationListItem'; import { BaseConversationListItem } from './BaseConversationListItem';
import { ConversationType } from '../../state/ducks/conversations';
export type PropsDataType = { export type PropsDataType = {
isSelected?: boolean; isSelected?: boolean;
@ -29,15 +29,19 @@ export type PropsDataType = {
body: string; body: string;
bodyRanges: BodyRangesType; bodyRanges: BodyRangesType;
from: { from: Pick<
phoneNumber?: string; ConversationType,
title: string; | 'acceptedMessageRequest'
isMe?: boolean; | 'avatarPath'
name?: string; | 'color'
color?: ColorType; | 'isMe'
profileName?: string; | 'name'
avatarPath?: string; | 'phoneNumber'
}; | 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
to: { to: {
groupName?: string; groupName?: string;
@ -192,6 +196,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={from.acceptedMessageRequest}
avatarPath={from.avatarPath} avatarPath={from.avatarPath}
color={from.color} color={from.color}
conversationType="direct" conversationType="direct"
@ -207,8 +212,10 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
onClick={onClickItem} onClick={onClickItem}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
profileName={from.profileName} profileName={from.profileName}
sharedGroupNames={from.sharedGroupNames}
style={style} style={style}
title={from.title} title={from.title}
unblurredAvatarPath={from.unblurredAvatarPath}
/> />
); );
} }

9
ts/model-types.d.ts vendored
View file

@ -283,6 +283,15 @@ export type ConversationAttributesType = {
// Used only when user is waiting for approval to join via link // Used only when user is waiting for approval to join via link
isTemporary?: boolean; isTemporary?: boolean;
temporaryMemberCount?: number; temporaryMemberCount?: number;
// Avatars are blurred for some unapproved conversations, but users can manually unblur
// them. If the avatar was unblurred and then changed, we don't update this value so
// the new avatar gets blurred.
//
// This value is useless once the message request has been approved. We don't clean it
// up but could. We don't persist it but could (though we'd probably want to clean it
// up in that case).
unblurredAvatarPath?: string;
}; };
export type GroupV2MemberType = { export type GroupV2MemberType = {

View file

@ -1340,6 +1340,7 @@ export class ConversationModel extends window.Backbone
canChangeTimer: this.canChangeTimer(), canChangeTimer: this.canChangeTimer(),
canEditGroupInfo: this.canEditGroupInfo(), canEditGroupInfo: this.canEditGroupInfo(),
avatarPath: this.getAbsoluteAvatarPath(), avatarPath: this.getAbsoluteAvatarPath(),
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
color, color,
discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'), discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'),
draftBodyRanges, draftBodyRanges,
@ -4883,16 +4884,32 @@ export class ConversationModel extends window.Backbone
return migrateColor(this.get('color')); return migrateColor(this.get('color'));
} }
getAbsoluteAvatarPath(): string | undefined { private getAvatarPath(): undefined | string {
const avatar = this.isMe() const avatar = this.isMe()
? this.get('profileAvatar') || this.get('avatar') ? this.get('profileAvatar') || this.get('avatar')
: this.get('avatar') || this.get('profileAvatar'); : this.get('avatar') || this.get('profileAvatar');
return avatar?.path || undefined;
if (!avatar || !avatar.path) {
return undefined;
} }
return getAbsoluteAttachmentPath(avatar.path); getAbsoluteAvatarPath(): string | undefined {
const avatarPath = this.getAvatarPath();
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
}
getAbsoluteUnblurredAvatarPath(): string | undefined {
const unblurredAvatarPath = this.get('unblurredAvatarPath');
return unblurredAvatarPath
? getAbsoluteAttachmentPath(unblurredAvatarPath)
: undefined;
}
unblurAvatar(): void {
const avatarPath = this.getAvatarPath();
if (avatarPath) {
this.set('unblurredAvatarPath', avatarPath);
} else {
this.unset('unblurredAvatarPath');
}
} }
private canChangeTimer(): boolean { private canChangeTimer(): boolean {

View file

@ -2326,7 +2326,9 @@ async function saveConversation(
` `
).run({ ).run({
id, id,
json: objectToJSON(omit(data, ['profileLastFetchedAt'])), json: objectToJSON(
omit(data, ['profileLastFetchedAt', 'unblurredAvatarPath'])
),
e164: e164 || null, e164: e164 || null,
uuid: uuid || null, uuid: uuid || null,
@ -2399,7 +2401,9 @@ async function updateConversation(data: ConversationType): Promise<void> {
` `
).run({ ).run({
id, id,
json: objectToJSON(omit(data, ['profileLastFetchedAt'])), json: objectToJSON(
omit(data, ['profileLastFetchedAt', 'unblurredAvatarPath'])
),
e164: e164 || null, e164: e164 || null,
uuid: uuid || null, uuid: uuid || null,

View file

@ -60,6 +60,7 @@ export type ConversationType = {
profileName?: string; profileName?: string;
about?: string; about?: string;
avatarPath?: string; avatarPath?: string;
unblurredAvatarPath?: string;
areWeAdmin?: boolean; areWeAdmin?: boolean;
areWePending?: boolean; areWePending?: boolean;
areWePendingApproval?: boolean; areWePendingApproval?: boolean;

View file

@ -105,6 +105,8 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
'title', 'title',
'type', 'type',
'groupVersion', 'groupVersion',
'sharedGroupNames',
'unblurredAvatarPath',
]), ]),
conversationTitle: state.conversations.selectedConversationTitle, conversationTitle: state.conversations.selectedConversationTitle,
isMissingMandatoryProfileSharing: Boolean( isMissingMandatoryProfileSharing: Boolean(

View file

@ -70,12 +70,14 @@ function renderLastSeenIndicator(id: string): JSX.Element {
function renderHeroRow( function renderHeroRow(
id: string, id: string,
onHeightChange: () => unknown, onHeightChange: () => unknown,
unblurAvatar: () => void,
updateSharedGroups: () => unknown updateSharedGroups: () => unknown
): JSX.Element { ): JSX.Element {
return ( return (
<FilteredSmartHeroRow <FilteredSmartHeroRow
id={id} id={id}
onHeightChange={onHeightChange} onHeightChange={onHeightChange}
unblurAvatar={unblurAvatar}
updateSharedGroups={updateSharedGroups} updateSharedGroups={updateSharedGroups}
/> />
); );

View file

@ -0,0 +1,104 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
describe('shouldBlurAvatar', () => {
it('returns false for me', () => {
assert.isFalse(
shouldBlurAvatar({
isMe: true,
acceptedMessageRequest: false,
avatarPath: '/path/to/avatar.jpg',
sharedGroupNames: [],
unblurredAvatarPath: undefined,
})
);
});
it('returns false if the message request has been accepted', () => {
assert.isFalse(
shouldBlurAvatar({
acceptedMessageRequest: true,
avatarPath: '/path/to/avatar.jpg',
isMe: false,
sharedGroupNames: [],
unblurredAvatarPath: undefined,
})
);
});
it('returns false if there are any shared groups', () => {
assert.isFalse(
shouldBlurAvatar({
sharedGroupNames: ['Tahoe Trip'],
acceptedMessageRequest: false,
avatarPath: '/path/to/avatar.jpg',
isMe: false,
unblurredAvatarPath: undefined,
})
);
});
it('returns false if there is no avatar', () => {
assert.isFalse(
shouldBlurAvatar({
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
unblurredAvatarPath: undefined,
})
);
assert.isFalse(
shouldBlurAvatar({
avatarPath: undefined,
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
unblurredAvatarPath: undefined,
})
);
assert.isFalse(
shouldBlurAvatar({
avatarPath: undefined,
unblurredAvatarPath: '/some/other/path',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
it('returns false if the avatar was unblurred', () => {
assert.isFalse(
shouldBlurAvatar({
avatarPath: '/path/to/avatar.jpg',
unblurredAvatarPath: '/path/to/avatar.jpg',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
it('returns true if the stars align (i.e., not everything above)', () => {
assert.isTrue(
shouldBlurAvatar({
avatarPath: '/path/to/avatar.jpg',
acceptedMessageRequest: false,
isMe: false,
})
);
assert.isTrue(
shouldBlurAvatar({
avatarPath: '/path/to/avatar.jpg',
unblurredAvatarPath: '/different/path.jpg',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
});

View file

@ -16510,7 +16510,7 @@
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/conversation/ConversationHero.js", "path": "ts/components/conversation/ConversationHero.js",
"line": " const firstRenderRef = React.useRef(true);", "line": " const firstRenderRef = React.useRef(true);",
"lineNumber": 48, "lineNumber": 49,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z", "updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element." "reasonDetail": "Doesn't refer to a DOM element."
@ -16519,7 +16519,7 @@
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/conversation/ConversationHero.tsx", "path": "ts/components/conversation/ConversationHero.tsx",
"line": " const firstRenderRef = React.useRef(true);", "line": " const firstRenderRef = React.useRef(true);",
"lineNumber": 77, "lineNumber": 84,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z", "updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element." "reasonDetail": "Doesn't refer to a DOM element."
@ -16582,7 +16582,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();", "line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 250, "lineNumber": 254,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z", "updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for managing focus only" "reasonDetail": "Used for managing focus only"
@ -16591,7 +16591,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();", "line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
"lineNumber": 252, "lineNumber": 256,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z", "updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button" "reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
@ -16600,7 +16600,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();", "line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 254, "lineNumber": 258,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z", "updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for detecting clicks outside reaction viewer" "reasonDetail": "Used for detecting clicks outside reaction viewer"

View file

@ -0,0 +1,28 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ConversationType } from '../state/ducks/conversations';
export const shouldBlurAvatar = ({
acceptedMessageRequest,
avatarPath,
isMe,
sharedGroupNames = [],
unblurredAvatarPath,
}: Readonly<
Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'isMe'
| 'sharedGroupNames'
| 'unblurredAvatarPath'
>
>): boolean =>
Boolean(
!isMe &&
!acceptedMessageRequest &&
!sharedGroupNames.length &&
avatarPath &&
avatarPath !== unblurredAvatarPath
);

View file

@ -976,6 +976,9 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
onShowContactModal: this.showContactModal.bind(this), onShowContactModal: this.showContactModal.bind(this),
scrollToQuotedMessage, scrollToQuotedMessage,
unblurAvatar: () => {
this.model.unblurAvatar();
},
updateSharedGroups: this.model.throttledUpdateSharedGroups, updateSharedGroups: this.model.throttledUpdateSharedGroups,
}), }),
}); });