Blur avatars of unapproved conversations
This commit is contained in:
parent
bbd7fd3854
commit
05703c2719
28 changed files with 474 additions and 124 deletions
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { isBoolean } from 'lodash';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select, text } from '@storybook/addon-knobs';
|
||||
|
@ -30,6 +31,9 @@ const conversationTypeMap: Record<string, Props['conversationType']> = {
|
|||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest)
|
||||
? overrideProps.acceptedMessageRequest
|
||||
: true,
|
||||
avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
|
||||
blur: overrideProps.blur,
|
||||
color: select('color', colorMap, overrideProps.color || 'blue'),
|
||||
|
@ -151,7 +155,16 @@ story.add('Loading', () => {
|
|||
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({
|
||||
avatarPath: '/fixtures/kitten-3-64-64.jpg',
|
||||
blur: AvatarBlur.BlurPicture,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { LocalizerType } from '../types/Util';
|
|||
import { ColorType } from '../types/Colors';
|
||||
import * as log from '../logging/log';
|
||||
import { assert } from '../util/assert';
|
||||
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
|
||||
|
||||
export enum AvatarBlur {
|
||||
NoBlur,
|
||||
|
@ -39,13 +40,17 @@ export type Props = {
|
|||
color?: ColorType;
|
||||
loading?: boolean;
|
||||
|
||||
acceptedMessageRequest?: boolean;
|
||||
conversationType: 'group' | 'direct';
|
||||
noteToSelf?: boolean;
|
||||
title: string;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
noteToSelf?: boolean;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
sharedGroupNames?: Array<string>;
|
||||
size: AvatarSize;
|
||||
title: string;
|
||||
unblurredAvatarPath?: string;
|
||||
|
||||
onClick?: () => unknown;
|
||||
|
||||
|
@ -55,19 +60,34 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
} & Pick<React.HTMLProps<HTMLDivElement>, 'className'>;
|
||||
|
||||
const getDefaultBlur = (
|
||||
...args: Parameters<typeof shouldBlurAvatar>
|
||||
): AvatarBlur =>
|
||||
shouldBlurAvatar(...args) ? AvatarBlur.BlurPicture : AvatarBlur.NoBlur;
|
||||
|
||||
export const Avatar: FunctionComponent<Props> = ({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
className,
|
||||
color,
|
||||
conversationType,
|
||||
i18n,
|
||||
isMe,
|
||||
innerRef,
|
||||
loading,
|
||||
noteToSelf,
|
||||
onClick,
|
||||
sharedGroupNames,
|
||||
size,
|
||||
title,
|
||||
blur = AvatarBlur.NoBlur,
|
||||
unblurredAvatarPath,
|
||||
blur = getDefaultBlur({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
isMe,
|
||||
sharedGroupNames,
|
||||
unblurredAvatarPath,
|
||||
}),
|
||||
}) => {
|
||||
const [imageBroken, setImageBroken] = useState(false);
|
||||
|
||||
|
@ -111,6 +131,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
);
|
||||
} else if (hasImage) {
|
||||
assert(avatarPath, 'avatarPath should be defined here');
|
||||
|
||||
assert(
|
||||
blur !== AvatarBlur.BlurPictureWithClickToView || size >= 100,
|
||||
'Rendering "click to view" for a small avatar. This may not render correctly'
|
||||
|
|
|
@ -6,17 +6,21 @@ import { LocalizerType } from '../types/Util';
|
|||
import { Avatar } from './Avatar';
|
||||
import { Intl } from './Intl';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
type Props = {
|
||||
conversation: {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
conversation: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
i18n: LocalizerType;
|
||||
close: () => void;
|
||||
};
|
||||
|
@ -39,6 +43,7 @@ export const CallNeedPermissionScreen: React.FC<Props> = ({
|
|||
return (
|
||||
<div className="module-call-need-permission-screen">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPath={conversation.avatarPath}
|
||||
color={conversation.color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
|
|
|
@ -10,36 +10,45 @@ import { Emojify } from './conversation/Emojify';
|
|||
import { InContactsIcon } from './InContactsIcon';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
type Props = {
|
||||
about?: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
i18n: LocalizerType;
|
||||
isAdmin?: boolean;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
onClick?: () => void;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'about'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
export class ContactListItem extends React.Component<Props> {
|
||||
public renderAvatar(): JSX.Element {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
i18n,
|
||||
color,
|
||||
i18n,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
|
@ -48,7 +57,9 @@ export class ContactListItem extends React.Component<Props> {
|
|||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={52}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,26 +3,33 @@
|
|||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
|
||||
export type PropsType = {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
firstName?: string;
|
||||
i18n: LocalizerType;
|
||||
id: string;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
onClickRemove: (id: string) => void;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'about'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'firstName'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
export const ContactPill: FunctionComponent<PropsType> = ({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
firstName,
|
||||
|
@ -31,7 +38,9 @@ export const ContactPill: FunctionComponent<PropsType> = ({
|
|||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
onClickRemove,
|
||||
}) => {
|
||||
const removeLabel = i18n('ContactPill--remove');
|
||||
|
@ -39,6 +48,7 @@ export const ContactPill: FunctionComponent<PropsType> = ({
|
|||
return (
|
||||
<div className="module-ContactPill">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
noteToSelf={false}
|
||||
|
@ -48,7 +58,9 @@ export const ContactPill: FunctionComponent<PropsType> = ({
|
|||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
<ContactName
|
||||
firstName={firstName}
|
||||
|
|
|
@ -86,6 +86,7 @@ export const SafetyNumberChangeDialog = ({
|
|||
key={contact.id}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
|
@ -94,7 +95,9 @@ export const SafetyNumberChangeDialog = ({
|
|||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
title={contact.title}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={52}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
/>
|
||||
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
|
||||
<div className="module-SafetyNumberChangeDialog__contact--name">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactPortal } from 'react';
|
||||
|
@ -106,14 +106,17 @@ export const ContactModal = ({
|
|||
aria-label={i18n('close')}
|
||||
/>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={96}
|
||||
title={contact.title}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
/>
|
||||
<div className="module-contact-modal__name">{contact.title}</div>
|
||||
<div className="module-about__container">
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Avatar, AvatarSize } from '../Avatar';
|
|||
import { InContactsIcon } from '../InContactsIcon';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
|
||||
import {
|
||||
ExpirationTimerOptions,
|
||||
|
@ -35,33 +35,33 @@ export enum OutgoingCallButtonStyle {
|
|||
|
||||
export type PropsDataType = {
|
||||
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;
|
||||
left?: boolean;
|
||||
markedUnread?: boolean;
|
||||
groupVersion?: number;
|
||||
|
||||
canChangeTimer?: boolean;
|
||||
expireTimer?: number;
|
||||
muteExpiresAt?: number;
|
||||
|
||||
showBackButton?: boolean;
|
||||
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 = {
|
||||
onSetMuteNotifications: (seconds: number) => void;
|
||||
|
@ -180,6 +180,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
private renderAvatar(): ReactNode {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
i18n,
|
||||
|
@ -188,22 +189,28 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<span className="module-ConversationHeader__header__avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
noteToSelf={isMe}
|
||||
title={title}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number as numberKnob, text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
|
@ -33,6 +34,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -50,6 +52,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -67,6 +70,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={['NYC Rock Climbers']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -84,6 +88,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -101,6 +106,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -118,6 +124,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -134,6 +141,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
phoneNumber={text('phoneNumber', '')}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -147,6 +155,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
conversationType="group"
|
||||
membersCount={numberKnob('membersCount', 22)}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -160,6 +169,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
conversationType="group"
|
||||
membersCount={1}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -173,6 +183,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
conversationType="group"
|
||||
membersCount={0}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -186,6 +197,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
name={text('groupName', '')}
|
||||
conversationType="group"
|
||||
membersCount={0}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -199,6 +211,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
title={getTitle()}
|
||||
conversationType="direct"
|
||||
phoneNumber={getPhoneNumber()}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 { About } from './About';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
|
||||
export type Props = {
|
||||
about?: string;
|
||||
acceptedMessageRequest?: boolean;
|
||||
i18n: LocalizerType;
|
||||
isMe?: boolean;
|
||||
sharedGroupNames?: Array<string>;
|
||||
membersCount?: number;
|
||||
phoneNumber?: string;
|
||||
onHeightChange?: () => unknown;
|
||||
unblurAvatar: () => void;
|
||||
unblurredAvatarPath?: string;
|
||||
updateSharedGroups?: () => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
|
@ -61,6 +65,7 @@ const renderMembershipRow = ({
|
|||
export const ConversationHero = ({
|
||||
i18n,
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
conversationType,
|
||||
|
@ -72,6 +77,8 @@ export const ConversationHero = ({
|
|||
profileName,
|
||||
title,
|
||||
onHeightChange,
|
||||
unblurAvatar,
|
||||
unblurredAvatarPath,
|
||||
updateSharedGroups,
|
||||
}: Props): JSX.Element => {
|
||||
const firstRenderRef = React.useRef(true);
|
||||
|
@ -106,6 +113,23 @@ export const ConversationHero = ({
|
|||
]);
|
||||
/* 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(
|
||||
!name && !profileName && conversationType === 'direct'
|
||||
);
|
||||
|
@ -115,11 +139,13 @@ export const ConversationHero = ({
|
|||
<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}
|
||||
|
|
|
@ -111,13 +111,17 @@ export type PropsData = {
|
|||
contact?: ContactType;
|
||||
author: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
reducedMotion?: boolean;
|
||||
conversationType: ConversationTypesType;
|
||||
|
@ -1159,15 +1163,19 @@ export class Message extends React.Component<Props, State> {
|
|||
tabIndex={0}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={author.acceptedMessageRequest}
|
||||
avatarPath={author.avatarPath}
|
||||
color={author.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={author.isMe}
|
||||
name={author.name}
|
||||
phoneNumber={author.phoneNumber}
|
||||
profileName={author.profileName}
|
||||
title={author.title}
|
||||
sharedGroupNames={author.sharedGroupNames}
|
||||
size={28}
|
||||
title={author.title}
|
||||
unblurredAvatarPath={author.unblurredAvatarPath}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -271,6 +271,8 @@ const actions = () => ({
|
|||
onBlockAndDelete: action('onBlockAndDelete'),
|
||||
onDelete: action('onDelete'),
|
||||
onUnblock: action('onUnblock'),
|
||||
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
});
|
||||
|
||||
const renderItem = (id: string) => (
|
||||
|
@ -312,6 +314,7 @@ const renderHeroRow = () => (
|
|||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
/>
|
||||
);
|
||||
const renderLoadingRow = () => <TimelineLoadingRow state="loading" />;
|
||||
|
|
|
@ -81,6 +81,7 @@ type PropsHousekeepingType = {
|
|||
renderHeroRow: (
|
||||
id: string,
|
||||
resizeHeroRow: () => unknown,
|
||||
unblurAvatar: () => void,
|
||||
updateSharedGroups: () => unknown
|
||||
) => JSX.Element;
|
||||
renderLoadingRow: (id: string) => JSX.Element;
|
||||
|
@ -113,6 +114,7 @@ type PropsActionsType = {
|
|||
onUnblock: () => unknown;
|
||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||
clearSelectedMessage: () => unknown;
|
||||
unblurAvatar: () => void;
|
||||
updateSharedGroups: () => unknown;
|
||||
} & MessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
@ -583,6 +585,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
renderLoadingRow,
|
||||
renderLastSeenIndicator,
|
||||
renderTypingBubble,
|
||||
unblurAvatar,
|
||||
updateSharedGroups,
|
||||
} = this.props;
|
||||
const { lastMeasuredWarningHeight } = this.state;
|
||||
|
@ -602,7 +605,12 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
{this.getWarning() ? (
|
||||
<div style={{ height: lastMeasuredWarningHeight }} />
|
||||
) : null}
|
||||
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
|
||||
{renderHeroRow(
|
||||
id,
|
||||
this.resizeHeroRow,
|
||||
unblurAvatar,
|
||||
updateSharedGroups
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (!haveOldest && row === 0) {
|
||||
|
|
|
@ -9,8 +9,8 @@ import { Avatar, AvatarSize } from '../Avatar';
|
|||
import { Timestamp } from '../conversation/Timestamp';
|
||||
import { isConversationUnread } from '../../util/isConversationUnread';
|
||||
import { cleanId } from '../_util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
const BASE_CLASS_NAME =
|
||||
'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`;
|
||||
|
||||
type PropsType = {
|
||||
avatarPath?: string;
|
||||
checked?: boolean;
|
||||
color?: ColorType;
|
||||
conversationType: 'group' | 'direct';
|
||||
disabled?: boolean;
|
||||
headerDate?: number;
|
||||
headerName: ReactNode;
|
||||
i18n: LocalizerType;
|
||||
id?: string;
|
||||
isMe?: boolean;
|
||||
i18n: LocalizerType;
|
||||
isNoteToSelf?: boolean;
|
||||
isSelected: boolean;
|
||||
markedUnread?: boolean;
|
||||
messageId?: string;
|
||||
messageStatusIcon?: ReactNode;
|
||||
messageText?: ReactNode;
|
||||
name?: string;
|
||||
onClick?: () => void;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
style: CSSProperties;
|
||||
title: string;
|
||||
unreadCount?: number;
|
||||
};
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'markedUnread'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
|
||||
({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
checked,
|
||||
color,
|
||||
|
@ -69,8 +76,10 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
style,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
}) => {
|
||||
const isUnread = isConversationUnread({ markedUnread, unreadCount });
|
||||
|
@ -112,6 +121,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
<>
|
||||
<div className={`${BASE_CLASS_NAME}__avatar-container`}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
|
@ -121,7 +131,9 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FIFTY_TWO}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
{isUnread && (
|
||||
<div className={`${BASE_CLASS_NAME}__unread-count`}>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import React, { CSSProperties, FunctionComponent, ReactNode } from 'react';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ContactName } from '../conversation/ContactName';
|
||||
import { About } from '../conversation/About';
|
||||
|
@ -17,18 +17,23 @@ export enum ContactCheckboxDisabledReason {
|
|||
}
|
||||
|
||||
export type PropsDataType = {
|
||||
about?: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
disabledReason?: ContactCheckboxDisabledReason;
|
||||
id: string;
|
||||
isMe?: boolean;
|
||||
isChecked: boolean;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'about'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -44,6 +49,7 @@ type PropsType = PropsDataType & PropsHousekeepingType;
|
|||
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
({
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
disabledReason,
|
||||
|
@ -55,8 +61,10 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
|||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
style,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
}) => {
|
||||
const disabled = Boolean(disabledReason);
|
||||
|
||||
|
@ -87,6 +95,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
|||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
checked={isChecked}
|
||||
color={color}
|
||||
|
@ -102,8 +111,10 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
|||
onClick={onClickItem}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
style={style}
|
||||
title={title}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,23 +4,27 @@
|
|||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ContactName } from '../conversation/ContactName';
|
||||
import { About } from '../conversation/About';
|
||||
|
||||
export type PropsDataType = {
|
||||
about?: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
id: string;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
type: 'group' | 'direct';
|
||||
};
|
||||
export type PropsDataType = Pick<
|
||||
ConversationType,
|
||||
| 'about'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -33,6 +37,7 @@ type PropsType = PropsDataType & PropsHousekeepingType;
|
|||
export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||
({
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
i18n,
|
||||
|
@ -42,9 +47,11 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
style,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarPath,
|
||||
}) => {
|
||||
const headerName = isMe ? (
|
||||
i18n('noteToSelf')
|
||||
|
@ -63,6 +70,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
|
@ -76,8 +84,10 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
onClick={onClick ? () => onClick(id) : undefined}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
style={style}
|
||||
title={title}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,8 @@ export type PropsData = {
|
|||
avatarPath?: string;
|
||||
isMe?: boolean;
|
||||
muteExpiresAt?: number;
|
||||
sharedGroupNames?: Array<string>;
|
||||
unblurredAvatarPath?: string;
|
||||
|
||||
lastUpdated?: number;
|
||||
unreadCount?: number;
|
||||
|
@ -89,11 +91,13 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
shouldShowDraft,
|
||||
style,
|
||||
title,
|
||||
type,
|
||||
typingContact,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
}) => {
|
||||
const headerName = isMe ? (
|
||||
|
@ -180,6 +184,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
|
@ -196,9 +201,11 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
onClick={onClickItem}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
style={style}
|
||||
title={title}
|
||||
unreadCount={unreadCount}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, {
|
||||
|
@ -14,8 +14,8 @@ import { ContactName } from '../conversation/ContactName';
|
|||
|
||||
import { assert } from '../../util/assert';
|
||||
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
export type PropsDataType = {
|
||||
isSelected?: boolean;
|
||||
|
@ -29,15 +29,19 @@ export type PropsDataType = {
|
|||
body: string;
|
||||
bodyRanges: BodyRangesType;
|
||||
|
||||
from: {
|
||||
phoneNumber?: string;
|
||||
title: string;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
color?: ColorType;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
};
|
||||
from: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
|
||||
to: {
|
||||
groupName?: string;
|
||||
|
@ -192,6 +196,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
|||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={from.acceptedMessageRequest}
|
||||
avatarPath={from.avatarPath}
|
||||
color={from.color}
|
||||
conversationType="direct"
|
||||
|
@ -207,8 +212,10 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
|||
onClick={onClickItem}
|
||||
phoneNumber={from.phoneNumber}
|
||||
profileName={from.profileName}
|
||||
sharedGroupNames={from.sharedGroupNames}
|
||||
style={style}
|
||||
title={from.title}
|
||||
unblurredAvatarPath={from.unblurredAvatarPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
9
ts/model-types.d.ts
vendored
9
ts/model-types.d.ts
vendored
|
@ -283,6 +283,15 @@ export type ConversationAttributesType = {
|
|||
// Used only when user is waiting for approval to join via link
|
||||
isTemporary?: boolean;
|
||||
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 = {
|
||||
|
|
|
@ -1340,6 +1340,7 @@ export class ConversationModel extends window.Backbone
|
|||
canChangeTimer: this.canChangeTimer(),
|
||||
canEditGroupInfo: this.canEditGroupInfo(),
|
||||
avatarPath: this.getAbsoluteAvatarPath(),
|
||||
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
|
||||
color,
|
||||
discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'),
|
||||
draftBodyRanges,
|
||||
|
@ -4883,16 +4884,32 @@ export class ConversationModel extends window.Backbone
|
|||
return migrateColor(this.get('color'));
|
||||
}
|
||||
|
||||
getAbsoluteAvatarPath(): string | undefined {
|
||||
private getAvatarPath(): undefined | string {
|
||||
const avatar = this.isMe()
|
||||
? this.get('profileAvatar') || this.get('avatar')
|
||||
: this.get('avatar') || this.get('profileAvatar');
|
||||
return avatar?.path || undefined;
|
||||
}
|
||||
|
||||
if (!avatar || !avatar.path) {
|
||||
return undefined;
|
||||
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');
|
||||
}
|
||||
|
||||
return getAbsoluteAttachmentPath(avatar.path);
|
||||
}
|
||||
|
||||
private canChangeTimer(): boolean {
|
||||
|
|
|
@ -2326,7 +2326,9 @@ async function saveConversation(
|
|||
`
|
||||
).run({
|
||||
id,
|
||||
json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
|
||||
json: objectToJSON(
|
||||
omit(data, ['profileLastFetchedAt', 'unblurredAvatarPath'])
|
||||
),
|
||||
|
||||
e164: e164 || null,
|
||||
uuid: uuid || null,
|
||||
|
@ -2399,7 +2401,9 @@ async function updateConversation(data: ConversationType): Promise<void> {
|
|||
`
|
||||
).run({
|
||||
id,
|
||||
json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
|
||||
json: objectToJSON(
|
||||
omit(data, ['profileLastFetchedAt', 'unblurredAvatarPath'])
|
||||
),
|
||||
|
||||
e164: e164 || null,
|
||||
uuid: uuid || null,
|
||||
|
|
|
@ -60,6 +60,7 @@ export type ConversationType = {
|
|||
profileName?: string;
|
||||
about?: string;
|
||||
avatarPath?: string;
|
||||
unblurredAvatarPath?: string;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
areWePendingApproval?: boolean;
|
||||
|
|
|
@ -105,6 +105,8 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
|
|||
'title',
|
||||
'type',
|
||||
'groupVersion',
|
||||
'sharedGroupNames',
|
||||
'unblurredAvatarPath',
|
||||
]),
|
||||
conversationTitle: state.conversations.selectedConversationTitle,
|
||||
isMissingMandatoryProfileSharing: Boolean(
|
||||
|
|
|
@ -70,12 +70,14 @@ function renderLastSeenIndicator(id: string): JSX.Element {
|
|||
function renderHeroRow(
|
||||
id: string,
|
||||
onHeightChange: () => unknown,
|
||||
unblurAvatar: () => void,
|
||||
updateSharedGroups: () => unknown
|
||||
): JSX.Element {
|
||||
return (
|
||||
<FilteredSmartHeroRow
|
||||
id={id}
|
||||
onHeightChange={onHeightChange}
|
||||
unblurAvatar={unblurAvatar}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
/>
|
||||
);
|
||||
|
|
104
ts/test-both/util/shouldBlurAvatar_test.ts
Normal file
104
ts/test-both/util/shouldBlurAvatar_test.ts
Normal 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: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -16510,7 +16510,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ConversationHero.js",
|
||||
"line": " const firstRenderRef = React.useRef(true);",
|
||||
"lineNumber": 48,
|
||||
"lineNumber": 49,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Doesn't refer to a DOM element."
|
||||
|
@ -16519,7 +16519,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ConversationHero.tsx",
|
||||
"line": " const firstRenderRef = React.useRef(true);",
|
||||
"lineNumber": 77,
|
||||
"lineNumber": 84,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Doesn't refer to a DOM element."
|
||||
|
@ -16582,7 +16582,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 250,
|
||||
"lineNumber": 254,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for managing focus only"
|
||||
|
@ -16591,7 +16591,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||
"lineNumber": 252,
|
||||
"lineNumber": 256,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||
|
@ -16600,7 +16600,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 254,
|
||||
"lineNumber": 258,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||
|
@ -16936,4 +16936,4 @@
|
|||
"updated": "2021-01-08T15:46:32.143Z",
|
||||
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
|
||||
}
|
||||
]
|
||||
]
|
28
ts/util/shouldBlurAvatar.ts
Normal file
28
ts/util/shouldBlurAvatar.ts
Normal 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
|
||||
);
|
|
@ -976,6 +976,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
onShowContactModal: this.showContactModal.bind(this),
|
||||
scrollToQuotedMessage,
|
||||
unblurAvatar: () => {
|
||||
this.model.unblurAvatar();
|
||||
},
|
||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||
}),
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue