Click to download avatar for unaccepted conversations

This commit is contained in:
yash-signal
2025-03-26 13:27:04 -07:00
committed by GitHub
parent 6c5047ba3e
commit 7cf26c5e25
100 changed files with 730 additions and 544 deletions

View File

@@ -21,7 +21,7 @@ import { getAuthorId } from './messages/helpers';
import { maybeDeriveGroupV2Id } from './groups'; import { maybeDeriveGroupV2Id } from './groups';
import { assertDev, strictAssert } from './util/assert'; import { assertDev, strictAssert } from './util/assert';
import { drop } from './util/drop'; import { drop } from './util/drop';
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; import { isGroup, isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
import type { ServiceIdString, AciString, PniString } from './types/ServiceId'; import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
import { import {
isServiceIdString, isServiceIdString,
@@ -39,6 +39,8 @@ import * as StorageService from './services/storage';
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats'; import type { ConversationPropsForUnreadStats } from './util/countUnreadStats';
import { countAllConversationsUnreadStats } from './util/countUnreadStats'; import { countAllConversationsUnreadStats } from './util/countUnreadStats';
import { isTestOrMockEnvironment } from './environment'; import { isTestOrMockEnvironment } from './environment';
import { isConversationAccepted } from './util/isConversationAccepted';
import { areWePending } from './util/groupMembershipUtils';
import { conversationJobQueue } from './jobs/conversationJobQueue'; import { conversationJobQueue } from './jobs/conversationJobQueue';
type ConvoMatchType = type ConvoMatchType =
@@ -1372,6 +1374,52 @@ export class ConversationController {
this.get(conversationId)?.onOpenComplete(loadStart); this.get(conversationId)?.onOpenComplete(loadStart);
} }
migrateAvatarsForNonAcceptedConversations(): void {
if (window.storage.get('avatarsHaveBeenMigrated')) {
return;
}
const conversations = this.getAll();
let numberOfConversationsMigrated = 0;
for (const conversation of conversations) {
const attrs = conversation.attributes;
if (
!isConversationAccepted(attrs) ||
(isGroup(attrs) && areWePending(attrs))
) {
const avatarPath = attrs.avatar?.path;
const profileAvatarPath = attrs.profileAvatar?.path;
if (avatarPath || profileAvatarPath) {
drop(
(async () => {
const { doesAttachmentExist, deleteAttachmentData } =
window.Signal.Migrations;
if (avatarPath && (await doesAttachmentExist(avatarPath))) {
await deleteAttachmentData(avatarPath);
}
if (
profileAvatarPath &&
(await doesAttachmentExist(profileAvatarPath))
) {
await deleteAttachmentData(profileAvatarPath);
}
})()
);
}
conversation.set('avatar', undefined);
conversation.set('profileAvatar', undefined);
drop(updateConversation(conversation.attributes));
numberOfConversationsMigrated += 1;
}
}
log.info(
`ConversationController: unset avatars for ${numberOfConversationsMigrated} unaccepted conversations`
);
drop(window.storage.put('avatarsHaveBeenMigrated', true));
}
repairPinnedConversations(): void { repairPinnedConversations(): void {
const pinnedIds = window.storage.get('pinnedConversationIds', []); const pinnedIds = window.storage.get('pinnedConversationIds', []);

View File

@@ -670,6 +670,34 @@ export function constantTimeEqual(
return crypto.constantTimeEqual(left, right); return crypto.constantTimeEqual(left, right);
} }
export function getIdentifierHash({
aci,
e164,
pni,
groupId,
}: {
aci: AciString | undefined;
e164: string | undefined;
pni: PniString | undefined;
groupId: string | undefined;
}): number | null {
let identifier: Uint8Array;
if (aci != null) {
identifier = Aci.parseFromServiceIdString(aci).getServiceIdBinary();
} else if (e164 != null) {
identifier = Bytes.fromString(e164);
} else if (pni != null) {
identifier = Pni.parseFromServiceIdString(pni).getServiceIdBinary();
} else if (groupId != null) {
identifier = Bytes.fromBase64(groupId);
} else {
return null;
}
const digest = hash(HashType.size256, identifier);
return digest[0];
}
export function generateAvatarColor({ export function generateAvatarColor({
aci, aci,
e164, e164,
@@ -681,19 +709,11 @@ export function generateAvatarColor({
pni: PniString | undefined; pni: PniString | undefined;
groupId: string | undefined; groupId: string | undefined;
}): string { }): string {
let identifier: Uint8Array; const hashValue = getIdentifierHash({ aci, e164, pni, groupId });
if (aci != null) {
identifier = Aci.parseFromServiceIdString(aci).getServiceIdBinary(); if (hashValue == null) {
} else if (e164 != null) {
identifier = Bytes.fromString(e164);
} else if (pni != null) {
identifier = Pni.parseFromServiceIdString(pni).getServiceIdBinary();
} else if (groupId != null) {
identifier = Bytes.fromBase64(groupId);
} else {
return sample(AvatarColors) || AvatarColors[0]; return sample(AvatarColors) || AvatarColors[0];
} }
const digest = hash(HashType.size256, identifier); return AvatarColors[hashValue % AVATAR_COLOR_COUNT];
return AvatarColors[digest[0] % AVATAR_COLOR_COUNT];
} }

View File

@@ -1445,6 +1445,10 @@ export async function startApp(): Promise<void> {
if (window.isBeforeVersion(lastVersion, 'v5.31.0')) { if (window.isBeforeVersion(lastVersion, 'v5.31.0')) {
window.ConversationController.repairPinnedConversations(); window.ConversationController.repairPinnedConversations();
} }
if (!window.storage.get('avatarsHaveBeenMigrated', false)) {
window.ConversationController.migrateAvatarsForNonAcceptedConversations();
}
} }
void badgeImageFileDownloader.checkForFilesToDownload(); void badgeImageFileDownloader.checkForFilesToDownload();

View File

@@ -129,7 +129,7 @@ export function AddUserToAnotherGroupModal({
} }
return { return {
...pick(convo, 'id', 'avatarUrl', 'title', 'unblurredAvatarUrl'), ...pick(convo, 'id', 'avatarUrl', 'title', 'hasAvatar'),
memberships, memberships,
membersCount, membersCount,
disabledReason, disabledReason,

View File

@@ -4,7 +4,6 @@
import type { Meta, StoryFn } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
import * as React from 'react'; import * as React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { isBoolean } from 'lodash';
import { expect, fn, within, userEvent } from '@storybook/test'; import { expect, fn, within, userEvent } from '@storybook/test';
import type { AvatarColorType } from '../types/Colors'; import type { AvatarColorType } from '../types/Colors';
import type { Props } from './Avatar'; import type { Props } from './Avatar';
@@ -60,16 +59,13 @@ export default {
} satisfies Meta<Props>; } satisfies Meta<Props>;
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest)
? overrideProps.acceptedMessageRequest
: true,
avatarUrl: overrideProps.avatarUrl || '', avatarUrl: overrideProps.avatarUrl || '',
badge: overrideProps.badge, badge: overrideProps.badge,
blur: overrideProps.blur, blur: overrideProps.blur,
color: overrideProps.color || AvatarColors[0], color: overrideProps.color || AvatarColors[0],
conversationType: overrideProps.conversationType || 'direct', conversationType: overrideProps.conversationType || 'direct',
hasAvatar: Boolean(overrideProps.hasAvatar),
i18n, i18n,
isMe: false,
loading: Boolean(overrideProps.loading), loading: Boolean(overrideProps.loading),
noteToSelf: Boolean(overrideProps.noteToSelf), noteToSelf: Boolean(overrideProps.noteToSelf),
onClick: fn(action('onClick')), onClick: fn(action('onClick')),
@@ -200,8 +196,9 @@ Loading.args = createProps({
export const BlurredBasedOnProps = TemplateSingle.bind({}); export const BlurredBasedOnProps = TemplateSingle.bind({});
BlurredBasedOnProps.args = createProps({ BlurredBasedOnProps.args = createProps({
acceptedMessageRequest: false, hasAvatar: true,
avatarUrl: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
blur: AvatarBlur.BlurPicture,
}); });
export const ForceBlurred = TemplateSingle.bind({}); export const ForceBlurred = TemplateSingle.bind({});

View File

@@ -25,8 +25,8 @@ import { assertDev } from '../util/assert';
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath'; import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { isBadgeVisible } from '../badges/isBadgeVisible'; import { isBadgeVisible } from '../badges/isBadgeVisible';
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
import { SIGNAL_AVATAR_PATH } from '../types/SignalConversation'; import { SIGNAL_AVATAR_PATH } from '../types/SignalConversation';
import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient';
export enum AvatarBlur { export enum AvatarBlur {
NoBlur, NoBlur,
@@ -54,20 +54,18 @@ type BadgePlacementType = { bottom: number; right: number };
export type Props = { export type Props = {
avatarUrl?: string; avatarUrl?: string;
avatarPlaceholderGradient?: Readonly<[string, string]>;
blur?: AvatarBlur; blur?: AvatarBlur;
color?: AvatarColorType; color?: AvatarColorType;
hasAvatar?: boolean;
loading?: boolean; loading?: boolean;
acceptedMessageRequest: boolean;
conversationType: 'group' | 'direct' | 'callLink'; conversationType: 'group' | 'direct' | 'callLink';
isMe: boolean;
noteToSelf?: boolean; noteToSelf?: boolean;
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
sharedGroupNames: ReadonlyArray<string>; sharedGroupNames: ReadonlyArray<string>;
size: AvatarSize; size: AvatarSize;
title: string; title: string;
unblurredAvatarUrl?: string;
searchResult?: boolean; searchResult?: boolean;
storyRing?: HasStories; storyRing?: HasStories;
@@ -100,39 +98,26 @@ const BADGE_PLACEMENT_BY_SIZE = new Map<number, BadgePlacementType>([
[112, { bottom: -4, right: 3 }], [112, { bottom: -4, right: 3 }],
]); ]);
const getDefaultBlur = (
...args: Parameters<typeof shouldBlurAvatar>
): AvatarBlur =>
shouldBlurAvatar(...args) ? AvatarBlur.BlurPicture : AvatarBlur.NoBlur;
export function Avatar({ export function Avatar({
acceptedMessageRequest,
avatarUrl, avatarUrl,
avatarPlaceholderGradient = getAvatarPlaceholderGradient(0),
badge, badge,
className, className,
color = 'A200', color = 'A200',
conversationType, conversationType,
hasAvatar,
i18n, i18n,
isMe,
innerRef, innerRef,
loading, loading,
noteToSelf, noteToSelf,
onClick, onClick,
onClickBadge, onClickBadge,
sharedGroupNames,
size, size,
theme, theme,
title, title,
unblurredAvatarUrl,
searchResult, searchResult,
storyRing, storyRing,
blur = getDefaultBlur({ blur = AvatarBlur.NoBlur,
acceptedMessageRequest,
avatarUrl,
isMe,
sharedGroupNames,
unblurredAvatarUrl,
}),
...ariaProps ...ariaProps
}: Props): JSX.Element { }: Props): JSX.Element {
const [imageBroken, setImageBroken] = useState(false); const [imageBroken, setImageBroken] = useState(false);
@@ -204,6 +189,20 @@ export function Avatar({
)} )}
</> </>
); );
} else if (hasAvatar && !hasImage) {
contentsChildren = (
<>
<div
className="module-Avatar__image"
style={{
backgroundImage: `linear-gradient(to bottom, ${avatarPlaceholderGradient[0]}, ${avatarPlaceholderGradient[1]})`,
}}
/>
{blur === AvatarBlur.BlurPictureWithClickToView && (
<div className="module-Avatar__click-to-view">{i18n('icu:view')}</div>
)}
</>
);
} else if (searchResult) { } else if (searchResult) {
contentsChildren = ( contentsChildren = (
<div <div

View File

@@ -10,9 +10,11 @@ import { Lightbox } from './Lightbox';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
export type PropsType = { export type PropsType = {
avatarPlaceholderGradient?: Readonly<[string, string]>;
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarUrl?: string; avatarUrl?: string;
conversationTitle?: string; conversationTitle?: string;
hasAvatar?: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isGroup?: boolean; isGroup?: boolean;
noteToSelf?: boolean; noteToSelf?: boolean;
@@ -20,9 +22,11 @@ export type PropsType = {
}; };
export function AvatarLightbox({ export function AvatarLightbox({
avatarPlaceholderGradient,
avatarColor, avatarColor,
avatarUrl, avatarUrl,
conversationTitle, conversationTitle,
hasAvatar,
i18n, i18n,
isGroup, isGroup,
noteToSelf, noteToSelf,
@@ -44,9 +48,11 @@ export function AvatarLightbox({
selectedIndex={0} selectedIndex={0}
> >
<AvatarPreview <AvatarPreview
avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarColor={avatarColor} avatarColor={avatarColor}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
conversationTitle={conversationTitle} conversationTitle={conversationTitle}
hasAvatar={hasAvatar}
i18n={i18n} i18n={i18n}
isGroup={isGroup} isGroup={isGroup}
noteToSelf={noteToSelf} noteToSelf={noteToSelf}

View File

@@ -12,6 +12,7 @@ import type { AvatarColorType } from '../types/Colors';
import { AvatarColors } from '../types/Colors'; import { AvatarColors } from '../types/Colors';
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { imagePathToBytes } from '../util/imagePathToBytes'; import { imagePathToBytes } from '../util/imagePathToBytes';
import { type ConversationType } from '../state/ducks/conversations';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
@@ -26,19 +27,22 @@ export type PropsType = {
onClear?: () => unknown; onClear?: () => unknown;
onClick?: () => unknown; onClick?: () => unknown;
style?: CSSProperties; style?: CSSProperties;
}; } & Pick<ConversationType, 'avatarPlaceholderGradient' | 'hasAvatar'>;
enum ImageStatus { enum ImageStatus {
Nothing = 'nothing', Nothing = 'nothing',
Loading = 'loading', Loading = 'loading',
HasImage = 'has-image', HasImage = 'has-image',
HasPlaceholder = 'has-placeholder',
} }
export function AvatarPreview({ export function AvatarPreview({
avatarPlaceholderGradient,
avatarColor = AvatarColors[0], avatarColor = AvatarColors[0],
avatarUrl, avatarUrl,
avatarValue, avatarValue,
conversationTitle, conversationTitle,
hasAvatar,
i18n, i18n,
isEditable, isEditable,
isGroup, isGroup,
@@ -127,6 +131,8 @@ export function AvatarPreview({
} else if (avatarUrl) { } else if (avatarUrl) {
encodedPath = avatarUrl; encodedPath = avatarUrl;
imageStatus = ImageStatus.HasImage; imageStatus = ImageStatus.HasImage;
} else if (hasAvatar && avatarPlaceholderGradient) {
imageStatus = ImageStatus.HasPlaceholder;
} else { } else {
imageStatus = ImageStatus.Nothing; imageStatus = ImageStatus.Nothing;
} }
@@ -184,6 +190,22 @@ export function AvatarPreview({
); );
} }
if (imageStatus === ImageStatus.HasPlaceholder) {
return (
<div className="AvatarPreview">
<div
className="AvatarPreview__avatar"
style={{
...componentStyle,
backgroundImage: avatarPlaceholderGradient
? `linear-gradient(to bottom, ${avatarPlaceholderGradient[0]}, ${avatarPlaceholderGradient[1]})`
: undefined,
}}
/>
</div>
);
}
return ( return (
<div className="AvatarPreview"> <div className="AvatarPreview">
<div <div

View File

@@ -91,8 +91,6 @@ export function CallLinkAddNameModal({
color={getColorForCallLink(callLink.rootKey)} color={getColorForCallLink(callLink.rootKey)}
conversationType="callLink" conversationType="callLink"
size={AvatarSize.SIXTY_FOUR} size={AvatarSize.SIXTY_FOUR}
acceptedMessageRequest
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
title={ title={
callLink.name === '' callLink.name === ''

View File

@@ -107,8 +107,6 @@ export function CallLinkDetails({
color={getColorForCallLink(callLink.rootKey)} color={getColorForCallLink(callLink.rootKey)}
conversationType="callLink" conversationType="callLink"
size={AvatarSize.SIXTY_FOUR} size={AvatarSize.SIXTY_FOUR}
acceptedMessageRequest
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
title={callLink.name ?? i18n('icu:calling__call-link-default-title')} title={callLink.name ?? i18n('icu:calling__call-link-default-title')}
/> />
@@ -277,8 +275,6 @@ function renderMissingCallLink({
badge={undefined} badge={undefined}
conversationType="callLink" conversationType="callLink"
size={AvatarSize.SIXTY_FOUR} size={AvatarSize.SIXTY_FOUR}
acceptedMessageRequest
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
title={i18n('icu:calling__call-link-default-title')} title={i18n('icu:calling__call-link-default-title')}
/> />

View File

@@ -128,8 +128,6 @@ export function CallLinkEditModal({
color={getColorForCallLink(callLink.rootKey)} color={getColorForCallLink(callLink.rootKey)}
conversationType="callLink" conversationType="callLink"
size={AvatarSize.SIXTY_FOUR} size={AvatarSize.SIXTY_FOUR}
acceptedMessageRequest
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
title={ title={
callLink.name === '' callLink.name === ''

View File

@@ -62,19 +62,18 @@ export function CallLinkPendingParticipantModal({
theme={Theme.Dark} theme={Theme.Dark}
> >
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
conversationType="direct" conversationType="direct"
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={conversation.isMe}
profileName={conversation.profileName} profileName={conversation.profileName}
sharedGroupNames={conversation.sharedGroupNames} sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.EIGHTY} size={AvatarSize.EIGHTY}
title={conversation.title} title={conversation.title}
theme={ThemeType.dark} theme={ThemeType.dark}
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
/> />
<button <button

View File

@@ -12,16 +12,17 @@ import type { ConversationType } from '../state/ducks/conversations';
export type Props = { export type Props = {
conversation: Pick< conversation: Pick<
ConversationType, ConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'hasAvatar'
| 'isMe' | 'isMe'
| 'name' | 'name'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarUrl'
>; >;
i18n: LocalizerType; i18n: LocalizerType;
close: () => void; close: () => void;
@@ -45,14 +46,14 @@ export function CallNeedPermissionScreen({
return ( return (
<div className="module-call-need-permission-screen"> <div className="module-call-need-permission-screen">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color || AvatarColors[0]} color={conversation.color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={conversation.isMe}
phoneNumber={conversation.phoneNumber} phoneNumber={conversation.phoneNumber}
profileName={conversation.profileName} profileName={conversation.profileName}
title={conversation.title} title={conversation.title}

View File

@@ -500,14 +500,14 @@ export function CallScreen({
) : ( ) : (
<CallBackgroundBlur avatarUrl={me.avatarUrl}> <CallBackgroundBlur avatarUrl={me.avatarUrl}>
<Avatar <Avatar
acceptedMessageRequest avatarPlaceholderGradient={me.avatarPlaceholderGradient}
avatarUrl={me.avatarUrl} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color || AvatarColors[0]} color={me.color || AvatarColors[0]}
hasAvatar={me.hasAvatar}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe
phoneNumber={me.phoneNumber} phoneNumber={me.phoneNumber}
profileName={me.profileName} profileName={me.profileName}
title={me.title} title={me.title}

View File

@@ -79,7 +79,7 @@ function UnknownContacts({
: 0; : 0;
return ( return (
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
className="CallingAdhocCallInfo__UnknownContactAvatar" className="CallingAdhocCallInfo__UnknownContactAvatar"
@@ -87,7 +87,6 @@ function UnknownContacts({
conversationType="direct" conversationType="direct"
key={key} key={key}
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}
@@ -210,13 +209,12 @@ export function CallingAdhocCallInfo({
> >
<div className="module-calling-participants-list__avatar-and-name"> <div className="module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}

View File

@@ -35,9 +35,11 @@ export type PropsType = {
callMode: CallMode; callMode: CallMode;
conversation: Pick< conversation: Pick<
CallingConversationType, CallingConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'hasAvatar'
| 'isMe' | 'isMe'
| 'memberships' | 'memberships'
| 'name' | 'name'
@@ -48,7 +50,6 @@ export type PropsType = {
| 'systemNickname' | 'systemNickname'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
>; >;
getIsSharingPhoneNumberWithEverybody: () => boolean; getIsSharingPhoneNumberWithEverybody: () => boolean;
groupMembers?: Array< groupMembers?: Array<

View File

@@ -126,15 +126,14 @@ export const CallingParticipantsList = React.memo(
> >
<div className="module-calling-participants-list__avatar-and-name"> <div className="module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={ avatarPlaceholderGradient={
participant.acceptedMessageRequest participant.avatarPlaceholderGradient
} }
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}

View File

@@ -308,13 +308,14 @@ export function CallingPendingParticipants({
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton" className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
> >
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} avatarPlaceholderGradient={
participant.avatarPlaceholderGradient
}
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}
@@ -396,13 +397,12 @@ export function CallingPendingParticipants({
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton" className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
> >
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}

View File

@@ -40,11 +40,10 @@ function NoVideo({
i18n: LocalizerType; i18n: LocalizerType;
}): JSX.Element { }): JSX.Element {
const { const {
acceptedMessageRequest, avatarPlaceholderGradient,
avatarUrl, avatarUrl,
color, color,
type: conversationType, type: conversationType,
isMe,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
@@ -56,14 +55,13 @@ function NoVideo({
<CallBackgroundBlur avatarUrl={avatarUrl}> <CallBackgroundBlur avatarUrl={avatarUrl}>
<div className="module-calling-pip__video--avatar"> <div className="module-calling-pip__video--avatar">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}
conversationType={conversationType} conversationType={conversationType}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}

View File

@@ -30,9 +30,11 @@ type PeekedParticipantType = Pick<
export type PropsType = { export type PropsType = {
conversation: Pick< conversation: Pick<
CallingConversationType, CallingConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'hasAvatar'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
@@ -41,7 +43,6 @@ export type PropsType = {
| 'systemNickname' | 'systemNickname'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
>; >;
i18n: LocalizerType; i18n: LocalizerType;
me: Pick<ConversationType, 'id' | 'serviceId'>; me: Pick<ConversationType, 'id' | 'serviceId'>;
@@ -216,19 +217,18 @@ export function CallingPreCallInfo({
return ( return (
<div className="module-CallingPreCallInfo"> <div className="module-CallingPreCallInfo">
<Avatar <Avatar
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
acceptedMessageRequest={conversation.acceptedMessageRequest}
conversationType={conversation.type} conversationType={conversation.type}
isMe={conversation.isMe} hasAvatar={conversation.hasAvatar}
noteToSelf={false} noteToSelf={false}
phoneNumber={conversation.phoneNumber} phoneNumber={conversation.phoneNumber}
profileName={conversation.profileName} profileName={conversation.profileName}
sharedGroupNames={conversation.sharedGroupNames} sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.SIXTY_FOUR} size={AvatarSize.SIXTY_FOUR}
title={conversation.title} title={conversation.title}
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
i18n={i18n} i18n={i18n}
/> />
<div className="module-CallingPreCallInfo__title"> <div className="module-CallingPreCallInfo__title">

View File

@@ -101,13 +101,15 @@ export function CallingRaisedHandsList({
> >
<div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name"> <div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} avatarPlaceholderGradient={
participant.avatarPlaceholderGradient
}
avatarUrl={participant.avatarUrl} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
hasAvatar={participant.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName} profileName={participant.profileName}
title={participant.title} title={participant.title}
sharedGroupNames={participant.sharedGroupNames} sharedGroupNames={participant.sharedGroupNames}

View File

@@ -900,12 +900,14 @@ export function CallsList({
aria-selected={isSelected} aria-selected={isSelected}
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest avatarPlaceholderGradient={
conversation.avatarPlaceholderGradient
}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
color={conversation.color} color={conversation.color}
conversationType={conversation.type} conversationType={conversation.type}
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={false}
title={conversation.title} title={conversation.title}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_SIX} size={AvatarSize.THIRTY_SIX}

View File

@@ -217,11 +217,12 @@ export function CallsNewCall({
<ListTile <ListTile
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest avatarPlaceholderGradient={
item.conversation.avatarPlaceholderGradient
}
avatarUrl={item.conversation.avatarUrl} avatarUrl={item.conversation.avatarUrl}
conversationType="group" conversationType="group"
i18n={i18n} i18n={i18n}
isMe={false}
title={item.conversation.title} title={item.conversation.title}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}

View File

@@ -14,32 +14,31 @@ export type PropsType = {
} & Pick< } & Pick<
ConversationType, ConversationType,
| 'about' | 'about'
| 'acceptedMessageRequest' | 'avatarPlaceholderGradient'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'firstName' | 'firstName'
| 'hasAvatar'
| 'id' | 'id'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarUrl'
>; >;
export function ContactPill({ export function ContactPill({
acceptedMessageRequest, avatarPlaceholderGradient,
avatarUrl, avatarUrl,
color, color,
firstName, firstName,
hasAvatar,
i18n, i18n,
isMe,
id, id,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarUrl,
onClickRemove, onClickRemove,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const removeLabel = i18n('icu:ContactPill--remove'); const removeLabel = i18n('icu:ContactPill--remove');
@@ -47,20 +46,19 @@ export function ContactPill({
return ( return (
<div className="module-ContactPill"> <div className="module-ContactPill">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color} color={color}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
hasAvatar={hasAvatar}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.TWENTY} size={AvatarSize.TWENTY}
unblurredAvatarUrl={unblurredAvatarUrl}
/> />
<ContactName <ContactName
firstName={firstName} firstName={firstName}

View File

@@ -38,6 +38,7 @@ const contactPillProps = (
getDefaultConversation({ getDefaultConversation({
avatarUrl: gifUrl, avatarUrl: gifUrl,
firstName: 'John', firstName: 'John',
hasAvatar: true,
id: 'abc123', id: 'abc123',
isMe: false, isMe: false,
name: 'John Bon Bon Jovi', name: 'John Bon Bon Jovi',

View File

@@ -402,12 +402,14 @@ export function ConversationList({
break; break;
case RowType.Conversation: { case RowType.Conversation: {
const itemProps = pick(row.conversation, [ const itemProps = pick(row.conversation, [
'avatarPlaceholderGradient',
'acceptedMessageRequest', 'acceptedMessageRequest',
'avatarUrl', 'avatarUrl',
'badges', 'badges',
'color', 'color',
'draftPreview', 'draftPreview',
'groupId', 'groupId',
'hasAvatar',
'id', 'id',
'isBlocked', 'isBlocked',
'isMe', 'isMe',
@@ -425,7 +427,6 @@ export function ConversationList({
'title', 'title',
'type', 'type',
'typingContactIdTimestamps', 'typingContactIdTimestamps',
'unblurredAvatarUrl',
'unreadCount', 'unreadCount',
'unreadMentionsCount', 'unreadMentionsCount',
'serviceId', 'serviceId',

View File

@@ -51,19 +51,21 @@ export function DirectCallRemoteParticipant({
function renderAvatar( function renderAvatar(
i18n: LocalizerType, i18n: LocalizerType,
{ {
acceptedMessageRequest, avatarPlaceholderGradient,
avatarUrl, avatarUrl,
color, color,
isMe, hasAvatar,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
}: Pick< }: Pick<
ConversationType, ConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'hasAvatar'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
@@ -75,14 +77,14 @@ function renderAvatar(
<div className="module-ongoing-call__remote-video-disabled"> <div className="module-ongoing-call__remote-video-disabled">
<CallBackgroundBlur avatarUrl={avatarUrl}> <CallBackgroundBlur avatarUrl={avatarUrl}>
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
hasAvatar={hasAvatar}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}

View File

@@ -90,16 +90,16 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
} = props; } = props;
const { const {
acceptedMessageRequest, avatarPlaceholderGradient,
addedTime, addedTime,
avatarUrl, avatarUrl,
color, color,
demuxId, demuxId,
hasAvatar,
hasRemoteAudio, hasRemoteAudio,
hasRemoteVideo, hasRemoteVideo,
isHandRaised, isHandRaised,
isBlocked, isBlocked,
isMe,
mediaKeysReceived, mediaKeysReceived,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
@@ -455,14 +455,14 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
} else { } else {
noVideoNode = ( noVideoNode = (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
hasAvatar={hasAvatar}
i18n={i18n} i18n={i18n}
isMe={isMe}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}

View File

@@ -109,16 +109,15 @@ function Contacts({
{contacts.map(contact => ( {contacts.map(contact => (
<li key={contact.id} className="module-GroupDialog__contacts__contact"> <li key={contact.id} className="module-GroupDialog__contacts__contact">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType={contact.type} conversationType={contact.type}
isMe={contact.isMe} hasAvatar={contact.hasAvatar}
noteToSelf={contact.isMe} noteToSelf={contact.isMe}
theme={theme} theme={theme}
title={contact.title} title={contact.title}
unblurredAvatarUrl={contact.unblurredAvatarUrl}
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
i18n={i18n} i18n={i18n}

View File

@@ -69,14 +69,12 @@ export const GroupV2JoinDialog = React.memo(function GroupV2JoinDialogInner({
/> />
<div className="module-group-v2-join-dialog__avatar"> <div className="module-group-v2-join-dialog__avatar">
<Avatar <Avatar
acceptedMessageRequest={false}
avatarUrl={avatar ? avatar.url : undefined} avatarUrl={avatar ? avatar.url : undefined}
badge={undefined} badge={undefined}
blur={AvatarBlur.NoBlur} blur={AvatarBlur.NoBlur}
loading={avatar && !avatar.url} loading={avatar && !avatar.url}
conversationType="group" conversationType="group"
title={title} title={title}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={80} size={80}
i18n={i18n} i18n={i18n}

View File

@@ -193,10 +193,8 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
} = props; } = props;
const { const {
id: conversationId, id: conversationId,
acceptedMessageRequest,
avatarUrl, avatarUrl,
color, color,
isMe,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
@@ -274,14 +272,12 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
<div className="IncomingCallBar__conversation"> <div className="IncomingCallBar__conversation">
<div className="IncomingCallBar__conversation--avatar"> <div className="IncomingCallBar__conversation--avatar">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}
conversationType={conversationType} conversationType={conversationType}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}

View File

@@ -174,18 +174,19 @@ export function LeftPaneSearchInput({
}} }}
> >
<Avatar <Avatar
acceptedMessageRequest={searchConversation.acceptedMessageRequest} avatarPlaceholderGradient={
searchConversation.avatarPlaceholderGradient
}
avatarUrl={searchConversation.avatarUrl} avatarUrl={searchConversation.avatarUrl}
badge={undefined} badge={undefined}
color={searchConversation.color} color={searchConversation.color}
conversationType={searchConversation.type} conversationType={searchConversation.type}
hasAvatar={searchConversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={searchConversation.isMe}
noteToSelf={searchConversation.isMe} noteToSelf={searchConversation.isMe}
sharedGroupNames={searchConversation.sharedGroupNames} sharedGroupNames={searchConversation.sharedGroupNames}
size={AvatarSize.TWENTY} size={AvatarSize.TWENTY}
title={searchConversation.title} title={searchConversation.title}
unblurredAvatarUrl={searchConversation.unblurredAvatarUrl}
/> />
<button <button
aria-label={i18n('icu:clearSearch')} aria-label={i18n('icu:clearSearch')}

View File

@@ -882,19 +882,18 @@ function LightboxHeader({
<div className="Lightbox__header--container"> <div className="Lightbox__header--container">
<div className="Lightbox__header--avatar"> <div className="Lightbox__header--avatar">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
conversationType={conversation.type} conversationType={conversation.type}
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={conversation.isMe}
phoneNumber={conversation.e164} phoneNumber={conversation.e164}
profileName={conversation.profileName} profileName={conversation.profileName}
sharedGroupNames={conversation.sharedGroupNames} sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={conversation.title} title={conversation.title}
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
/> />
</div> </div>
<div className="Lightbox__header--content"> <div className="Lightbox__header--content">

View File

@@ -48,15 +48,7 @@ export function MyStoryButton({
? getNewestMyStory(myStories[0]) ? getNewestMyStory(myStories[0])
: undefined; : undefined;
const { const { avatarUrl, color, profileName, sharedGroupNames, title } = me;
acceptedMessageRequest,
avatarUrl,
color,
isMe,
profileName,
sharedGroupNames,
title,
} = me;
if (!newestStory) { if (!newestStory) {
return ( return (
@@ -69,13 +61,11 @@ export function MyStoryButton({
> >
<div className="MyStories__avatar-container"> <div className="MyStories__avatar-container">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(isMe)}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.FORTY_EIGHT} size={AvatarSize.FORTY_EIGHT}
@@ -122,13 +112,11 @@ export function MyStoryButton({
onContextMenuShowingChanged={setActive} onContextMenuShowingChanged={setActive}
> >
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(isMe)}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.FORTY_EIGHT} size={AvatarSize.FORTY_EIGHT}

View File

@@ -372,14 +372,12 @@ export function NavTabs({
<span className="NavTabs__ItemButton"> <span className="NavTabs__ItemButton">
<span className="NavTabs__ItemContent"> <span className="NavTabs__ItemContent">
<Avatar <Avatar
acceptedMessageRequest
avatarUrl={me.avatarUrl} avatarUrl={me.avatarUrl}
badge={badge} badge={badge}
className="module-main-header__avatar" className="module-main-header__avatar"
color={me.color} color={me.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe
phoneNumber={me.phoneNumber} phoneNumber={me.phoneNumber}
profileName={me.profileName} profileName={me.profileName}
theme={theme} theme={theme}

View File

@@ -451,20 +451,19 @@ function ContactRow({
return ( return (
<li className="module-SafetyNumberChangeDialog__row" key={contact.id}> <li className="module-SafetyNumberChangeDialog__row" key={contact.id}>
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
profileName={contact.profileName} profileName={contact.profileName}
theme={theme} theme={theme}
title={contact.title} title={contact.title}
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<div className="module-SafetyNumberChangeDialog__row--wrapper"> <div className="module-SafetyNumberChangeDialog__row--wrapper">
<div className="module-SafetyNumberChangeDialog__row--name"> <div className="module-SafetyNumberChangeDialog__row--name">

View File

@@ -557,13 +557,14 @@ export function SendStoryModal({
htmlFor={id} htmlFor={id}
> >
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} avatarPlaceholderGradient={
group.avatarPlaceholderGradient
}
avatarUrl={group.avatarUrl} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={group.title} title={group.title}
@@ -707,13 +708,11 @@ export function SendStoryModal({
> >
{list.id === MY_STORY_ID ? ( {list.id === MY_STORY_ID ? (
<Avatar <Avatar
acceptedMessageRequest={me.acceptedMessageRequest}
avatarUrl={me.avatarUrl} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color} color={me.color}
conversationType={me.type} conversationType={me.type}
i18n={i18n} i18n={i18n}
isMe
sharedGroupNames={me.sharedGroupNames} sharedGroupNames={me.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
storyRing={undefined} storyRing={undefined}
@@ -822,13 +821,11 @@ export function SendStoryModal({
htmlFor={id} htmlFor={id}
> >
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest}
avatarUrl={group.avatarUrl} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
storyRing={group.hasStories} storyRing={group.hasStories}

View File

@@ -160,13 +160,12 @@ function DistributionListItem({
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
{isMyStory ? ( {isMyStory ? (
<Avatar <Avatar
acceptedMessageRequest={me.acceptedMessageRequest} avatarPlaceholderGradient={me.avatarPlaceholderGradient}
avatarUrl={me.avatarUrl} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color} color={me.color}
conversationType={me.type} conversationType={me.type}
i18n={i18n} i18n={i18n}
isMe
sharedGroupNames={me.sharedGroupNames} sharedGroupNames={me.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={me.title} title={me.title}
@@ -214,13 +213,12 @@ function GroupStoryItem({
> >
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={groupStory.acceptedMessageRequest} avatarPlaceholderGradient={groupStory.avatarPlaceholderGradient}
avatarUrl={groupStory.avatarUrl} avatarUrl={groupStory.avatarUrl}
badge={undefined} badge={undefined}
color={groupStory.color} color={groupStory.color}
conversationType={groupStory.type} conversationType={groupStory.type}
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={groupStory.title} title={groupStory.title}
@@ -675,13 +673,12 @@ export function DistributionListSettingsModal({
> >
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} avatarPlaceholderGradient={member.avatarPlaceholderGradient}
avatarUrl={member.avatarUrl} avatarUrl={member.avatarUrl}
badge={getPreferredBadge(member.badges)} badge={getPreferredBadge(member.badges)}
color={member.color} color={member.color}
conversationType={member.type} conversationType={member.type}
i18n={i18n} i18n={i18n}
isMe
sharedGroupNames={member.sharedGroupNames} sharedGroupNames={member.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
theme={theme} theme={theme}
@@ -1094,13 +1091,12 @@ export function EditDistributionListModal({
> >
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType={contact.type} conversationType={contact.type}
i18n={i18n} i18n={i18n}
isMe
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
theme={theme} theme={theme}
@@ -1190,10 +1186,10 @@ export function EditDistributionListModal({
{selectedContacts.map(contact => ( {selectedContacts.map(contact => (
<ContactPill <ContactPill
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.firstName} firstName={contact.firstName}
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
id={contact.id} id={contact.id}
isMe={contact.isMe} isMe={contact.isMe}
@@ -1287,13 +1283,12 @@ export function GroupStorySettingsModal({
> >
<div className="GroupStorySettingsModal__header"> <div className="GroupStorySettingsModal__header">
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} avatarPlaceholderGradient={group.avatarPlaceholderGradient}
avatarUrl={group.avatarUrl} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={group.title} title={group.title}
@@ -1316,13 +1311,12 @@ export function GroupStorySettingsModal({
className="GroupStorySettingsModal__members_item" className="GroupStorySettingsModal__members_item"
> >
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} avatarPlaceholderGradient={member.avatarPlaceholderGradient}
avatarUrl={member.avatarUrl} avatarUrl={member.avatarUrl}
badge={undefined} badge={undefined}
color={member.color} color={member.color}
conversationType={member.type} conversationType={member.type}
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={member.title} title={member.title}

View File

@@ -128,20 +128,17 @@ export function StoryDetailsModal({
return ( return (
<div key={contact.id} className="StoryDetailsModal__contact"> <div key={contact.id} className="StoryDetailsModal__contact">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
profileName={contact.profileName} profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
theme={ThemeType.dark} theme={ThemeType.dark}
title={contact.title} title={contact.title}
unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<div className="StoryDetailsModal__contact__text"> <div className="StoryDetailsModal__contact__text">
<ContactName title={contact.title} /> <ContactName title={contact.title} />
@@ -171,13 +168,12 @@ export function StoryDetailsModal({
</div> </div>
<div className="StoryDetailsModal__contact"> <div className="StoryDetailsModal__contact">
<Avatar <Avatar
acceptedMessageRequest={sender.acceptedMessageRequest} avatarPlaceholderGradient={sender.avatarPlaceholderGradient}
avatarUrl={sender.avatarUrl} avatarUrl={sender.avatarUrl}
badge={getPreferredBadge(sender.badges)} badge={getPreferredBadge(sender.badges)}
color={sender.color} color={sender.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={sender.isMe}
profileName={sender.profileName} profileName={sender.profileName}
sharedGroupNames={sender.sharedGroupNames} sharedGroupNames={sender.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}

View File

@@ -34,21 +34,20 @@ export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
}; };
function StoryListItemAvatar({ function StoryListItemAvatar({
acceptedMessageRequest, avatarPlaceholderGradient,
avatarUrl, avatarUrl,
avatarStoryRing, avatarStoryRing,
badges, badges,
color, color,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
isMe,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
theme, theme,
}: Pick< }: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'avatarPlaceholderGradient'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'profileName' | 'profileName'
@@ -59,18 +58,16 @@ function StoryListItemAvatar({
badges?: ConversationType['badges']; badges?: ConversationType['badges'];
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
isMe?: boolean;
theme: ThemeType; theme: ThemeType;
}): JSX.Element { }): JSX.Element {
return ( return (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={badges ? getPreferredBadge(badges) : undefined} badge={badges ? getPreferredBadge(badges) : undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(isMe)}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.FORTY_EIGHT} size={AvatarSize.FORTY_EIGHT}

View File

@@ -201,7 +201,6 @@ export function StoryViewer({
timestamp, timestamp,
} = story; } = story;
const { const {
acceptedMessageRequest,
avatarUrl, avatarUrl,
color, color,
isMe, isMe,
@@ -737,13 +736,11 @@ export function StoryViewer({
<div className="StoryViewer__meta__playback-bar"> <div className="StoryViewer__meta__playback-bar">
<div className="StoryViewer__meta__playback-bar__container"> <div className="StoryViewer__meta__playback-bar__container">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(isMe)}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
@@ -751,14 +748,12 @@ export function StoryViewer({
/> />
{group && ( {group && (
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest}
avatarUrl={group.avatarUrl} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
className="StoryViewer__meta--group-avatar" className="StoryViewer__meta--group-avatar"
color={getAvatarColor(group.color)} color={getAvatarColor(group.color)}
conversationType="group" conversationType="group"
i18n={i18n} i18n={i18n}
isMe={false}
profileName={group.profileName} profileName={group.profileName}
sharedGroupNames={group.sharedGroupNames} sharedGroupNames={group.sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}

View File

@@ -382,13 +382,11 @@ export function StoryViewsNRepliesModal({
> >
<div> <div>
<Avatar <Avatar
acceptedMessageRequest={view.recipient.acceptedMessageRequest}
avatarUrl={view.recipient.avatarUrl} avatarUrl={view.recipient.avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(view.recipient.color)} color={getAvatarColor(view.recipient.color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(view.recipient.isMe)}
profileName={view.recipient.profileName} profileName={view.recipient.profileName}
sharedGroupNames={view.recipient.sharedGroupNames || []} sharedGroupNames={view.recipient.sharedGroupNames || []}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
@@ -564,13 +562,11 @@ function ReplyOrReactionMessage({
> >
<div className="StoryViewsNRepliesModal__reaction--container"> <div className="StoryViewsNRepliesModal__reaction--container">
<Avatar <Avatar
acceptedMessageRequest={reply.author.acceptedMessageRequest}
avatarUrl={reply.author.avatarUrl} avatarUrl={reply.author.avatarUrl}
badge={getPreferredBadge(reply.author.badges)} badge={getPreferredBadge(reply.author.badges)}
color={getAvatarColor(reply.author.color)} color={getAvatarColor(reply.author.color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(reply.author.isMe)}
profileName={reply.author.profileName} profileName={reply.author.profileName}
sharedGroupNames={reply.author.sharedGroupNames || []} sharedGroupNames={reply.author.sharedGroupNames || []}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}

View File

@@ -68,7 +68,8 @@ export default {
toggleSafetyNumberModal: action('toggleSafetyNumberModal'), toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'), toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
updateSharedGroups: action('updateSharedGroups'), updateSharedGroups: action('updateSharedGroups'),
unblurAvatar: action('unblurAvatar'), startAvatarDownload: action('startAvatarDownload'),
pendingAvatarDownload: false,
conversation, conversation,
fromOrAddedByTrustedContact: false, fromOrAddedByTrustedContact: false,
isSignalConnection: false, isSignalConnection: false,

View File

@@ -1,11 +1,10 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { type ReactNode, useCallback, useEffect } from 'react'; import React, { type ReactNode, useCallback, useEffect, useMemo } from 'react';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { isInSystemContacts } from '../../util/isInSystemContacts'; import { isInSystemContacts } from '../../util/isInSystemContacts';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
import { Avatar, AvatarBlur, AvatarSize } from '../Avatar'; import { Avatar, AvatarBlur, AvatarSize } from '../Avatar';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { UserText } from '../UserText'; import { UserText } from '../UserText';
@@ -28,11 +27,12 @@ export type PropsType = Readonly<{
conversation: ConversationType; conversation: ConversationType;
fromOrAddedByTrustedContact?: boolean; fromOrAddedByTrustedContact?: boolean;
isSignalConnection: boolean; isSignalConnection: boolean;
pendingAvatarDownload?: boolean;
startAvatarDownload?: (id: string) => unknown;
toggleSignalConnectionsModal: () => void; toggleSignalConnectionsModal: () => void;
toggleSafetyNumberModal: (id: string) => void; toggleSafetyNumberModal: (id: string) => void;
toggleProfileNameWarningModal: () => void; toggleProfileNameWarningModal: () => void;
updateSharedGroups: (id: string) => void; updateSharedGroups: (id: string) => void;
unblurAvatar: (conversationId: string) => void;
}>; }>;
export function AboutContactModal({ export function AboutContactModal({
@@ -40,30 +40,44 @@ export function AboutContactModal({
conversation, conversation,
fromOrAddedByTrustedContact, fromOrAddedByTrustedContact,
isSignalConnection, isSignalConnection,
pendingAvatarDownload,
startAvatarDownload,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
toggleSafetyNumberModal, toggleSafetyNumberModal,
toggleProfileNameWarningModal, toggleProfileNameWarningModal,
updateSharedGroups, updateSharedGroups,
unblurAvatar,
onClose, onClose,
onOpenNotePreviewModal, onOpenNotePreviewModal,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const { isMe } = conversation; const { avatarUrl, hasAvatar, isMe } = conversation;
useEffect(() => { useEffect(() => {
// Kick off the expensive hydration of the current sharedGroupNames // Kick off the expensive hydration of the current sharedGroupNames
updateSharedGroups(conversation.id); updateSharedGroups(conversation.id);
}, [conversation.id, updateSharedGroups]); }, [conversation.id, updateSharedGroups]);
const avatarBlur = shouldBlurAvatar(conversation) // If hasAvatar is true, we show the download button instead of blur
const enableClickToLoad = !avatarUrl && !isMe && hasAvatar;
const avatarBlur = enableClickToLoad
? AvatarBlur.BlurPictureWithClickToView ? AvatarBlur.BlurPictureWithClickToView
: AvatarBlur.NoBlur; : AvatarBlur.NoBlur;
const onAvatarClick = useCallback(() => { const avatarOnClick = useMemo(() => {
if (avatarBlur === AvatarBlur.BlurPictureWithClickToView) { if (!enableClickToLoad) {
unblurAvatar(conversation.id); return undefined;
} }
}, [avatarBlur, unblurAvatar, conversation.id]); return () => {
if (!pendingAvatarDownload && startAvatarDownload) {
startAvatarDownload(conversation.id);
}
};
}, [
conversation.id,
startAvatarDownload,
enableClickToLoad,
pendingAvatarDownload,
]);
const onSignalConnectionClick = useCallback( const onSignalConnectionClick = useCallback(
(ev: React.MouseEvent) => { (ev: React.MouseEvent) => {
@@ -131,20 +145,20 @@ export function AboutContactModal({
> >
<div className="AboutContactModal__row AboutContactModal__row--centered"> <div className="AboutContactModal__row AboutContactModal__row--centered">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
blur={avatarBlur} blur={avatarBlur}
onClick={avatarBlur === AvatarBlur.NoBlur ? undefined : onAvatarClick} onClick={avatarOnClick}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
conversationType="direct" conversationType="direct"
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={conversation.isMe} loading={pendingAvatarDownload && !conversation.avatarUrl}
profileName={conversation.profileName} profileName={conversation.profileName}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.TWO_HUNDRED_SIXTEEN} size={AvatarSize.TWO_HUNDRED_SIXTEEN}
title={conversation.title} title={conversation.title}
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
/> />
</div> </div>

View File

@@ -53,6 +53,7 @@ export default {
), ),
removeMemberFromGroup: action('removeMemberFromGroup'), removeMemberFromGroup: action('removeMemberFromGroup'),
showConversation: action('showConversation'), showConversation: action('showConversation'),
startAvatarDownload: action('startAvatarDownload'),
theme: ThemeType.light, theme: ThemeType.light,
toggleAboutContactModal: action('AboutContactModal'), toggleAboutContactModal: action('AboutContactModal'),
toggleAdmin: action('toggleAdmin'), toggleAdmin: action('toggleAdmin'),

View File

@@ -14,7 +14,7 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import { StoryViewModeType } from '../../types/Stories'; import { StoryViewModeType } from '../../types/Stories';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { Avatar, AvatarSize } from '../Avatar'; import { Avatar, AvatarBlur, AvatarSize } from '../Avatar';
import { AvatarLightbox } from '../AvatarLightbox'; import { AvatarLightbox } from '../AvatarLightbox';
import { BadgeDialog } from '../BadgeDialog'; import { BadgeDialog } from '../BadgeDialog';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
@@ -55,6 +55,7 @@ type PropsActionType = {
onOutgoingVideoCallInConversation: (conversationId: string) => unknown; onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
removeMemberFromGroup: (conversationId: string, contactId: string) => void; removeMemberFromGroup: (conversationId: string, contactId: string) => void;
showConversation: ShowConversationType; showConversation: ShowConversationType;
startAvatarDownload: () => void;
toggleAdmin: (conversationId: string, contactId: string) => void; toggleAdmin: (conversationId: string, contactId: string) => void;
toggleAboutContactModal: (conversationId: string) => unknown; toggleAboutContactModal: (conversationId: string) => unknown;
togglePip: () => void; togglePip: () => void;
@@ -98,6 +99,7 @@ export function ContactModal({
onOutgoingVideoCallInConversation, onOutgoingVideoCallInConversation,
removeMemberFromGroup, removeMemberFromGroup,
showConversation, showConversation,
startAvatarDownload,
theme, theme,
toggleAboutContactModal, toggleAboutContactModal,
toggleAddUserToAnotherGroupModal, toggleAddUserToAnotherGroupModal,
@@ -310,13 +312,18 @@ export function ContactModal({
> >
<div className="ContactModal"> <div className="ContactModal">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={preferredBadge} badge={preferredBadge}
blur={
!contact.avatarUrl && !contact.isMe && contact.hasAvatar
? AvatarBlur.BlurPictureWithClickToView
: AvatarBlur.NoBlur
}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={contact.isMe}
onClick={() => { onClick={() => {
if (conversation && hasStories) { if (conversation && hasStories) {
viewUserStories({ viewUserStories({
@@ -324,6 +331,12 @@ export function ContactModal({
storyViewMode: StoryViewModeType.User, storyViewMode: StoryViewModeType.User,
}); });
hideContactModal(); hideContactModal();
} else if (
!contact.avatarUrl &&
!contact.isMe &&
contact.hasAvatar
) {
startAvatarDownload();
} else { } else {
setView(ContactModalView.ShowingAvatar); setView(ContactModalView.ShowingAvatar);
} }
@@ -335,7 +348,6 @@ export function ContactModal({
storyRing={hasStories} storyRing={hasStories}
theme={theme} theme={theme}
title={contact.title} title={contact.title}
unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<button <button
type="button" type="button"
@@ -468,9 +480,11 @@ export function ContactModal({
case ContactModalView.ShowingAvatar: case ContactModalView.ShowingAvatar:
return ( return (
<AvatarLightbox <AvatarLightbox
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarColor={contact.color} avatarColor={contact.color}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
conversationTitle={contact.title} conversationTitle={contact.title}
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
onClose={() => setView(ContactModalView.Default)} onClose={() => setView(ContactModalView.Default)}
/> />

View File

@@ -460,13 +460,17 @@ function HeaderContent({
const avatar = ( const avatar = (
<span className="module-ConversationHeader__header__avatar"> <span className="module-ConversationHeader__header__avatar">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} avatarPlaceholderGradient={
conversation.gradientStart && conversation.gradientEnd
? [conversation.gradientStart, conversation.gradientEnd]
: undefined
}
avatarUrl={conversation.avatarUrl ?? undefined} avatarUrl={conversation.avatarUrl ?? undefined}
badge={badge ?? undefined} badge={badge ?? undefined}
color={conversation.color ?? undefined} color={conversation.color ?? undefined}
conversationType={conversation.type} conversationType={conversation.type}
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={conversation.isMe}
noteToSelf={conversation.isMe} noteToSelf={conversation.isMe}
onClick={hasStories ? onViewUserStories : onClick} onClick={hasStories ? onViewUserStories : onClick}
phoneNumber={conversation.phoneNumber ?? undefined} phoneNumber={conversation.phoneNumber ?? undefined}
@@ -477,7 +481,6 @@ function HeaderContent({
storyRing={conversation.isMe ? undefined : (hasStories ?? undefined)} storyRing={conversation.isMe ? undefined : (hasStories ?? undefined)}
theme={theme} theme={theme}
title={conversation.title} title={conversation.title}
unblurredAvatarUrl={conversation.unblurredAvatarUrl ?? undefined}
/> />
</span> </span>
); );

View File

@@ -23,12 +23,13 @@ export default {
i18n, i18n,
isDirectConvoAndHasNickname: false, isDirectConvoAndHasNickname: false,
theme: ThemeType.light, theme: ThemeType.light,
unblurAvatar: action('unblurAvatar'),
updateSharedGroups: action('updateSharedGroups'), updateSharedGroups: action('updateSharedGroups'),
viewUserStories: action('viewUserStories'), viewUserStories: action('viewUserStories'),
toggleAboutContactModal: action('toggleAboutContactModal'), toggleAboutContactModal: action('toggleAboutContactModal'),
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'), toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
openConversationDetails: action('openConversationDetails'), openConversationDetails: action('openConversationDetails'),
startAvatarDownload: action('startAvatarDownload'),
pendingAvatarDownload: false,
}, },
} satisfies Meta<Props>; } satisfies Meta<Props>;

View File

@@ -13,7 +13,6 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import type { HasStories } from '../../types/Stories'; import type { HasStories } from '../../types/Stories';
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import { StoryViewModeType } from '../../types/Stories'; import { StoryViewModeType } from '../../types/Stories';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
import { Button, ButtonVariant } from '../Button'; import { Button, ButtonVariant } from '../Button';
import { SafetyTipsModal } from '../SafetyTipsModal'; import { SafetyTipsModal } from '../SafetyTipsModal';
import { I18n } from '../I18n'; import { I18n } from '../I18n';
@@ -23,6 +22,7 @@ export type Props = {
acceptedMessageRequest?: boolean; acceptedMessageRequest?: boolean;
fromOrAddedByTrustedContact?: boolean; fromOrAddedByTrustedContact?: boolean;
groupDescription?: string; groupDescription?: string;
hasAvatar?: boolean;
hasStories?: HasStories; hasStories?: HasStories;
id: string; id: string;
i18n: LocalizerType; i18n: LocalizerType;
@@ -31,10 +31,10 @@ export type Props = {
isSignalConversation?: boolean; isSignalConversation?: boolean;
membersCount?: number; membersCount?: number;
openConversationDetails?: () => unknown; openConversationDetails?: () => unknown;
pendingAvatarDownload?: boolean;
phoneNumber?: string; phoneNumber?: string;
sharedGroupNames?: ReadonlyArray<string>; sharedGroupNames?: ReadonlyArray<string>;
unblurAvatar: (conversationId: string) => void; startAvatarDownload: () => void;
unblurredAvatarUrl?: string;
updateSharedGroups: (conversationId: string) => unknown; updateSharedGroups: (conversationId: string) => unknown;
theme: ThemeType; theme: ThemeType;
viewUserStories: ViewUserStoriesActionCreatorType; viewUserStories: ViewUserStoriesActionCreatorType;
@@ -57,6 +57,7 @@ const renderExtraInformation = ({
sharedGroupNames, sharedGroupNames,
}: Pick< }: Pick<
Props, Props,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'conversationType' | 'conversationType'
| 'fromOrAddedByTrustedContact' | 'fromOrAddedByTrustedContact'
@@ -227,6 +228,7 @@ function ReleaseNotesExtraInformation({
} }
export function ConversationHero({ export function ConversationHero({
avatarPlaceholderGradient,
i18n, i18n,
about, about,
acceptedMessageRequest, acceptedMessageRequest,
@@ -236,6 +238,7 @@ export function ConversationHero({
conversationType, conversationType,
fromOrAddedByTrustedContact, fromOrAddedByTrustedContact,
groupDescription, groupDescription,
hasAvatar,
hasStories, hasStories,
id, id,
isDirectConvoAndHasNickname, isDirectConvoAndHasNickname,
@@ -243,13 +246,13 @@ export function ConversationHero({
openConversationDetails, openConversationDetails,
isSignalConversation, isSignalConversation,
membersCount, membersCount,
pendingAvatarDownload,
sharedGroupNames = [], sharedGroupNames = [],
phoneNumber, phoneNumber,
profileName, profileName,
startAvatarDownload,
theme, theme,
title, title,
unblurAvatar,
unblurredAvatarUrl,
updateSharedGroups, updateSharedGroups,
viewUserStories, viewUserStories,
toggleAboutContactModal, toggleAboutContactModal,
@@ -264,17 +267,14 @@ export function ConversationHero({
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur; let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
let avatarOnClick: undefined | (() => void); let avatarOnClick: undefined | (() => void);
if (
shouldBlurAvatar({ if (!avatarUrl && !isMe && hasAvatar) {
acceptedMessageRequest,
avatarUrl,
isMe,
sharedGroupNames,
unblurredAvatarUrl,
})
) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView; avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = () => unblurAvatar(id); avatarOnClick = () => {
if (!pendingAvatarDownload) {
startAvatarDownload();
}
};
} else if (hasStories) { } else if (hasStories) {
avatarOnClick = () => { avatarOnClick = () => {
viewUserStories({ viewUserStories({
@@ -312,7 +312,7 @@ export function ConversationHero({
<> <>
<div className="module-conversation-hero"> <div className="module-conversation-hero">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={badge} badge={badge}
blur={avatarBlur} blur={avatarBlur}
@@ -320,7 +320,8 @@ export function ConversationHero({
color={color} color={color}
conversationType={conversationType} conversationType={conversationType}
i18n={i18n} i18n={i18n}
isMe={isMe} hasAvatar={hasAvatar}
loading={pendingAvatarDownload && !avatarUrl}
noteToSelf={isMe} noteToSelf={isMe}
onClick={avatarOnClick} onClick={avatarOnClick}
profileName={profileName} profileName={profileName}

View File

@@ -279,18 +279,19 @@ export type PropsData = {
contact?: ReadonlyDeep<EmbeddedContactForUIType>; contact?: ReadonlyDeep<EmbeddedContactForUIType>;
author: Pick< author: Pick<
ConversationType, ConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'firstName' | 'firstName'
| 'hasAvatar'
| 'id' | 'id'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarUrl'
>; >;
conversationType: ConversationTypeType; conversationType: ConversationTypeType;
attachments?: ReadonlyArray<AttachmentForUIType>; attachments?: ReadonlyArray<AttachmentForUIType>;
@@ -1578,12 +1579,10 @@ export class Message extends React.PureComponent<Props, State> {
{first.isCallLink && ( {first.isCallLink && (
<div className="module-message__link-preview__call-link-icon"> <div className="module-message__link-preview__call-link-icon">
<Avatar <Avatar
acceptedMessageRequest
badge={undefined} badge={undefined}
color={getColorForCallLink(getKeyFromCallLink(first.url))} color={getColorForCallLink(getKeyFromCallLink(first.url))}
conversationType="callLink" conversationType="callLink"
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={64} size={64}
title={title ?? i18n('icu:calling__call-link-default-title')} title={title ?? i18n('icu:calling__call-link-default-title')}
@@ -2173,13 +2172,11 @@ export class Message extends React.PureComponent<Props, State> {
<AvatarSpacer size={GROUP_AVATAR_SIZE} /> <AvatarSpacer size={GROUP_AVATAR_SIZE} />
) : ( ) : (
<Avatar <Avatar
acceptedMessageRequest={author.acceptedMessageRequest}
avatarUrl={author.avatarUrl} avatarUrl={author.avatarUrl}
badge={getPreferredBadge(author.badges)} badge={getPreferredBadge(author.badges)}
color={author.color} color={author.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={author.isMe}
onClick={event => { onClick={event => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@@ -2192,7 +2189,6 @@ export class Message extends React.PureComponent<Props, State> {
size={GROUP_AVATAR_SIZE} size={GROUP_AVATAR_SIZE}
theme={theme} theme={theme}
title={author.title} title={author.title}
unblurredAvatarUrl={author.unblurredAvatarUrl}
/> />
)} )}
</div> </div>

View File

@@ -49,7 +49,6 @@ export type Contact = Pick<
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarUrl'
> & { > & {
status?: SendStatus; status?: SendStatus;
statusTimestamp?: number; statusTimestamp?: number;
@@ -168,34 +167,28 @@ export function MessageDetail({
function renderAvatar(contact: Contact): JSX.Element { function renderAvatar(contact: Contact): JSX.Element {
const { const {
acceptedMessageRequest,
avatarUrl, avatarUrl,
badges, badges,
color, color,
isMe,
phoneNumber, phoneNumber,
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarUrl,
} = contact; } = contact;
return ( return (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={getPreferredBadge(badges)} badge={getPreferredBadge(badges)}
color={color} color={color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
theme={theme} theme={theme}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarUrl={unblurredAvatarUrl}
/> />
); );
} }

View File

@@ -257,13 +257,11 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
> >
<div className="module-reaction-viewer__body__row__avatar"> <div className="module-reaction-viewer__body__row__avatar">
<Avatar <Avatar
acceptedMessageRequest={from.acceptedMessageRequest}
avatarUrl={from.avatarUrl} avatarUrl={from.avatarUrl}
badge={getPreferredBadge(from.badges)} badge={getPreferredBadge(from.badges)}
conversationType="direct" conversationType="direct"
sharedGroupNames={from.sharedGroupNames} sharedGroupNames={from.sharedGroupNames}
size={32} size={32}
isMe={from.isMe}
color={from.color} color={from.color}
profileName={from.profileName} profileName={from.profileName}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}

View File

@@ -124,12 +124,10 @@ export function Thumbnail({
return ( return (
<div className={getClassName('__icon-container-call-link')}> <div className={getClassName('__icon-container-call-link')}>
<Avatar <Avatar
acceptedMessageRequest
badge={undefined} badge={undefined}
color={getColorForCallLink(getKeyFromCallLink(url))} color={getColorForCallLink(getKeyFromCallLink(url))}
conversationType="callLink" conversationType="callLink"
i18n={i18n} i18n={i18n}
isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
size={64} size={64}
title={title ?? i18n('icu:calling__call-link-default-title')} title={title ?? i18n('icu:calling__call-link-default-title')}

View File

@@ -334,8 +334,6 @@ const actions = () => ({
closeContactSpoofingReview: action('closeContactSpoofingReview'), closeContactSpoofingReview: action('closeContactSpoofingReview'),
reviewConversationNameCollision: action('reviewConversationNameCollision'), reviewConversationNameCollision: action('reviewConversationNameCollision'),
unblurAvatar: action('unblurAvatar'),
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'), peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'), peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
@@ -346,6 +344,8 @@ const actions = () => ({
onOpenMessageRequestActionsConfirmation: action( onOpenMessageRequestActionsConfirmation: action(
'onOpenMessageRequestActionsConfirmation' 'onOpenMessageRequestActionsConfirmation'
), ),
startAvatarDownload: action('startAvatarDownload'),
}); });
const renderItem = ({ const renderItem = ({
@@ -416,7 +416,8 @@ const renderHeroRow = () => {
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']} sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
theme={theme} theme={theme}
title={getTitle()} title={getTitle()}
unblurAvatar={action('unblurAvatar')} startAvatarDownload={action('startAvatarDownload')}
pendingAvatarDownload={false}
updateSharedGroups={noop} updateSharedGroups={noop}
viewUserStories={action('viewUserStories')} viewUserStories={action('viewUserStories')}
toggleAboutContactModal={action('toggleAboutContactModal')} toggleAboutContactModal={action('toggleAboutContactModal')}

View File

@@ -123,13 +123,11 @@ function TypingBubbleAvatar({
return ( return (
<animated.div className="module-message__typing-avatar" style={springProps}> <animated.div className="module-message__typing-avatar" style={springProps}>
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={contact.isMe}
onClick={event => { onClick={event => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();

View File

@@ -34,14 +34,12 @@ export function renderAvatar({
const renderAttachmentDownloaded = () => ( const renderAttachmentDownloaded = () => (
<Avatar <Avatar
acceptedMessageRequest={false}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
blur={AvatarBlur.NoBlur} blur={AvatarBlur.NoBlur}
color={AvatarColors[0]} color={AvatarColors[0]}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe
title={title} title={title}
sharedGroupNames={[]} sharedGroupNames={[]}
size={size} size={size}

View File

@@ -411,10 +411,11 @@ export function ChooseGroupMembersModal({
{selectedContacts.map(contact => ( {selectedContacts.map(contact => (
<ContactPill <ContactPill
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest} avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName} firstName={contact.systemGivenName ?? contact.firstName}
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={contact.isMe} isMe={contact.isMe}
id={contact.id} id={contact.id}

View File

@@ -87,6 +87,7 @@ const createProps = (
pushPanelForConversation: action('pushPanelForConversation'), pushPanelForConversation: action('pushPanelForConversation'),
showConversation: action('showConversation'), showConversation: action('showConversation'),
showLightbox: action('showLightbox'), showLightbox: action('showLightbox'),
startAvatarDownload: action('startAvatarDownload'),
updateGroupAttributes: async () => { updateGroupAttributes: async () => {
action('updateGroupAttributes')(); action('updateGroupAttributes')();
}, },

View File

@@ -94,8 +94,10 @@ export type StateProps = {
maxRecommendedGroupSize: number; maxRecommendedGroupSize: number;
memberships: ReadonlyArray<GroupV2Membership>; memberships: ReadonlyArray<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>; pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingAvatarDownload?: boolean;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>; pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
selectedNavTab: NavTab; selectedNavTab: NavTab;
startAvatarDownload: () => void;
theme: ThemeType; theme: ThemeType;
userAvatarData: ReadonlyArray<AvatarDataType>; userAvatarData: ReadonlyArray<AvatarDataType>;
renderChooseGroupMembersModal: ( renderChooseGroupMembersModal: (
@@ -197,6 +199,7 @@ export function ConversationDetails({
onOutgoingAudioCallInConversation, onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation, onOutgoingVideoCallInConversation,
pendingApprovalMemberships, pendingApprovalMemberships,
pendingAvatarDownload,
pendingMemberships, pendingMemberships,
pushPanelForConversation, pushPanelForConversation,
renderChooseGroupMembersModal, renderChooseGroupMembersModal,
@@ -210,6 +213,7 @@ export function ConversationDetails({
showContactModal, showContactModal,
showConversation, showConversation,
showLightbox, showLightbox,
startAvatarDownload,
theme, theme,
toggleAboutContactModal, toggleAboutContactModal,
toggleSafetyNumberModal, toggleSafetyNumberModal,
@@ -410,6 +414,8 @@ export function ConversationDetails({
isGroup={isGroup} isGroup={isGroup}
isSignalConversation={isSignalConversation} isSignalConversation={isSignalConversation}
membersCount={conversation.membersCount ?? null} membersCount={conversation.membersCount ?? null}
pendingAvatarDownload={pendingAvatarDownload ?? false}
startAvatarDownload={startAvatarDownload}
startEditing={(isGroupTitle: boolean) => { startEditing={(isGroupTitle: boolean) => {
setModalState( setModalState(
isGroupTitle isGroupTitle

View File

@@ -43,6 +43,8 @@ function Wrapper(overrideProps: Partial<Props>) {
isGroup isGroup
isMe={false} isMe={false}
isSignalConversation={false} isSignalConversation={false}
pendingAvatarDownload={false}
startAvatarDownload={action('startAvatarDownload')}
theme={theme} theme={theme}
toggleAboutContactModal={action('toggleAboutContactModal')} toggleAboutContactModal={action('toggleAboutContactModal')}
{...overrideProps} {...overrideProps}

View File

@@ -4,7 +4,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Avatar, AvatarSize } from '../../Avatar'; import { Avatar, AvatarBlur, AvatarSize } from '../../Avatar';
import { AvatarLightbox } from '../../AvatarLightbox'; import { AvatarLightbox } from '../../AvatarLightbox';
import type { ConversationType } from '../../../state/ducks/conversations'; import type { ConversationType } from '../../../state/ducks/conversations';
import { GroupDescription } from '../GroupDescription'; import { GroupDescription } from '../GroupDescription';
@@ -27,6 +27,8 @@ export type Props = {
isMe: boolean; isMe: boolean;
isSignalConversation: boolean; isSignalConversation: boolean;
membersCount: number | null; membersCount: number | null;
pendingAvatarDownload: boolean;
startAvatarDownload: () => void;
startEditing: (isGroupTitle: boolean) => void; startEditing: (isGroupTitle: boolean) => void;
toggleAboutContactModal: (contactId: string) => void; toggleAboutContactModal: (contactId: string) => void;
theme: ThemeType; theme: ThemeType;
@@ -47,6 +49,8 @@ export function ConversationDetailsHeader({
isMe, isMe,
isSignalConversation, isSignalConversation,
membersCount, membersCount,
pendingAvatarDownload,
startAvatarDownload,
startEditing, startEditing,
toggleAboutContactModal, toggleAboutContactModal,
theme, theme,
@@ -84,8 +88,15 @@ export function ConversationDetailsHeader({
preferredBadge = badges?.[0]; preferredBadge = badges?.[0];
} }
const shouldShowClickToView =
!conversation.avatarUrl && !isMe && conversation.hasAvatar;
const avatarBlur = shouldShowClickToView
? AvatarBlur.BlurPictureWithClickToView
: AvatarBlur.NoBlur;
const avatar = ( const avatar = (
<Avatar <Avatar
blur={avatarBlur}
badge={preferredBadge} badge={preferredBadge}
conversationType={conversation.type} conversationType={conversation.type}
i18n={i18n} i18n={i18n}
@@ -93,9 +104,18 @@ export function ConversationDetailsHeader({
{...conversation} {...conversation}
noteToSelf={isMe} noteToSelf={isMe}
onClick={() => { onClick={() => {
if (shouldShowClickToView) {
startAvatarDownload();
return;
}
setActiveModal(ConversationDetailsHeaderActiveModal.ShowingAvatar); setActiveModal(ConversationDetailsHeaderActiveModal.ShowingAvatar);
}} }}
loading={pendingAvatarDownload}
onClickBadge={() => { onClickBadge={() => {
if (shouldShowClickToView) {
startAvatarDownload();
return;
}
setActiveModal(ConversationDetailsHeaderActiveModal.ShowingBadges); setActiveModal(ConversationDetailsHeaderActiveModal.ShowingBadges);
}} }}
sharedGroupNames={[]} sharedGroupNames={[]}
@@ -108,9 +128,11 @@ export function ConversationDetailsHeader({
case ConversationDetailsHeaderActiveModal.ShowingAvatar: case ConversationDetailsHeaderActiveModal.ShowingAvatar:
modal = ( modal = (
<AvatarLightbox <AvatarLightbox
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
avatarColor={conversation.color} avatarColor={conversation.color}
avatarUrl={conversation.avatarUrl} avatarUrl={conversation.avatarUrl}
conversationTitle={conversation.title} conversationTitle={conversation.title}
hasAvatar={conversation.hasAvatar}
i18n={i18n} i18n={i18n}
isGroup={isGroup} isGroup={isGroup}
noteToSelf={isMe} noteToSelf={isMe}

View File

@@ -58,17 +58,17 @@ type PropsType = {
testId?: string; testId?: string;
} & Pick< } & Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'avatarPlaceholderGradient'
| 'avatarUrl' | 'avatarUrl'
| 'color' | 'color'
| 'groupId' | 'groupId'
| 'hasAvatar'
| 'isMe' | 'isMe'
| 'markedUnread' | 'markedUnread'
| 'phoneNumber' | 'phoneNumber'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarUrl'
| 'serviceId' | 'serviceId'
> & > &
( (
@@ -79,7 +79,7 @@ type PropsType = {
export const BaseConversationListItem: FunctionComponent<PropsType> = export const BaseConversationListItem: FunctionComponent<PropsType> =
React.memo(function BaseConversationListItem(props) { React.memo(function BaseConversationListItem(props) {
const { const {
acceptedMessageRequest, avatarPlaceholderGradient,
avatarUrl, avatarUrl,
avatarSize, avatarSize,
buttonAriaLabel, buttonAriaLabel,
@@ -88,6 +88,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
conversationType, conversationType,
disabled, disabled,
groupId, groupId,
hasAvatar,
headerDate, headerDate,
headerName, headerName,
i18n, i18n,
@@ -108,7 +109,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
shouldShowSpinner, shouldShowSpinner,
testId: overrideTestId, testId: overrideTestId,
title, title,
unblurredAvatarUrl,
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
serviceId, serviceId,
@@ -195,20 +195,19 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
<> <>
<div className={AVATAR_CONTAINER_CLASS_NAME}> <div className={AVATAR_CONTAINER_CLASS_NAME}>
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={conversationType} conversationType={conversationType}
hasAvatar={hasAvatar}
noteToSelf={isAvatarNoteToSelf} noteToSelf={isAvatarNoteToSelf}
searchResult={isUsernameSearchResult} searchResult={isUsernameSearchResult}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={avatarSize ?? AvatarSize.FORTY_EIGHT} size={avatarSize ?? AvatarSize.FORTY_EIGHT}
unblurredAvatarUrl={unblurredAvatarUrl}
// This is here to appease the type checker. // This is here to appease the type checker.
{...(props.badge {...(props.badge
? { badge: props.badge, theme: props.theme } ? { badge: props.badge, theme: props.theme }

View File

@@ -37,7 +37,6 @@ export type PropsDataType = {
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
| 'serviceId' | 'serviceId'
>; >;
@@ -55,7 +54,6 @@ type PropsType = PropsDataType & PropsHousekeepingType;
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo( export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
function ContactCheckbox({ function ContactCheckbox({
about, about,
acceptedMessageRequest,
avatarUrl, avatarUrl,
badge, badge,
color, color,
@@ -71,7 +69,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
theme, theme,
title, title,
type, type,
unblurredAvatarUrl,
}) { }) {
const disabled = Boolean(disabledReason); const disabled = Boolean(disabledReason);
@@ -103,19 +100,16 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
isChecked={isChecked} isChecked={isChecked}
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={type} conversationType={type}
noteToSelf={Boolean(isMe)} noteToSelf={Boolean(isMe)}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarUrl={unblurredAvatarUrl}
// appease the type checker. // appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })} {...(badge ? { badge, theme } : { badge: undefined })}
/> />

View File

@@ -37,7 +37,6 @@ export type ContactListItemConversationType = Pick<
| 'systemFamilyName' | 'systemFamilyName'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
| 'username' | 'username'
| 'e164' | 'e164'
| 'serviceId' | 'serviceId'
@@ -63,7 +62,6 @@ type PropsType = PropsDataType & PropsHousekeepingType;
export const ContactListItem: FunctionComponent<PropsType> = React.memo( export const ContactListItem: FunctionComponent<PropsType> = React.memo(
function ContactListItem({ function ContactListItem({
about, about,
acceptedMessageRequest,
avatarUrl, avatarUrl,
badge, badge,
color, color,
@@ -85,7 +83,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
theme, theme,
title, title,
type, type,
unblurredAvatarUrl,
serviceId, serviceId,
}) { }) {
const [isConfirmingBlocking, setConfirmingBlocking] = useState(false); const [isConfirmingBlocking, setConfirmingBlocking] = useState(false);
@@ -264,19 +261,16 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
moduleClassName="ContactListItem" moduleClassName="ContactListItem"
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={type} conversationType={type}
noteToSelf={Boolean(isMe)} noteToSelf={Boolean(isMe)}
i18n={i18n} i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarUrl={unblurredAvatarUrl}
// This is here to appease the type checker. // This is here to appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })} {...(badge ? { badge, theme } : { badge: undefined })}
/> />

View File

@@ -38,12 +38,14 @@ export type MessageStatusType = (typeof MessageStatuses)[number];
export type PropsData = Pick< export type PropsData = Pick<
ConversationType, ConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'draftPreview' | 'draftPreview'
| 'groupId' | 'groupId'
| 'hasAvatar'
| 'id' | 'id'
| 'isBlocked' | 'isBlocked'
| 'isMe' | 'isMe'
@@ -62,7 +64,6 @@ export type PropsData = Pick<
| 'title' | 'title'
| 'type' | 'type'
| 'typingContactIdTimestamps' | 'typingContactIdTimestamps'
| 'unblurredAvatarUrl'
| 'unreadCount' | 'unreadCount'
| 'unreadMentionsCount' | 'unreadMentionsCount'
| 'serviceId' | 'serviceId'
@@ -82,6 +83,7 @@ export type Props = PropsData & PropsHousekeeping;
export const ConversationListItem: FunctionComponent<Props> = React.memo( export const ConversationListItem: FunctionComponent<Props> = React.memo(
function ConversationListItem({ function ConversationListItem({
avatarPlaceholderGradient,
acceptedMessageRequest, acceptedMessageRequest,
avatarUrl, avatarUrl,
badge, badge,
@@ -89,6 +91,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
color, color,
draftPreview, draftPreview,
groupId, groupId,
hasAvatar,
i18n, i18n,
id, id,
isBlocked, isBlocked,
@@ -109,7 +112,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
title, title,
type, type,
typingContactIdTimestamps, typingContactIdTimestamps,
unblurredAvatarUrl,
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
serviceId, serviceId,
@@ -211,13 +213,14 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={acceptedMessageRequest} avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
badge={badge} badge={badge}
buttonAriaLabel={buttonAriaLabel} buttonAriaLabel={buttonAriaLabel}
color={color} color={color}
conversationType={type} conversationType={type}
groupId={groupId} groupId={groupId}
hasAvatar={hasAvatar}
headerDate={lastUpdated} headerDate={lastUpdated}
headerName={headerName} headerName={headerName}
i18n={i18n} i18n={i18n}
@@ -237,7 +240,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
title={title} title={title}
unreadCount={unreadCount} unreadCount={unreadCount}
unreadMentionsCount={unreadMentionsCount} unreadMentionsCount={unreadMentionsCount}
unblurredAvatarUrl={unblurredAvatarUrl}
serviceId={serviceId} serviceId={serviceId}
/> />
); );

View File

@@ -16,7 +16,7 @@ export enum DisabledReason {
export type GroupListItemConversationType = Pick< export type GroupListItemConversationType = Pick<
ConversationType, ConversationType,
'id' | 'title' | 'avatarUrl' 'avatarPlaceholderGradient' | 'id' | 'title' | 'avatarUrl' | 'hasAvatar'
> & { > & {
disabledReason: DisabledReason | undefined; disabledReason: DisabledReason | undefined;
membersCount: number; membersCount: number;
@@ -55,11 +55,11 @@ export function GroupListItem({
<ListTile <ListTile
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest avatarPlaceholderGradient={group.avatarPlaceholderGradient}
avatarUrl={group.avatarUrl} avatarUrl={group.avatarUrl}
conversationType="group" conversationType="group"
hasAvatar={group.hasAvatar}
i18n={i18n} i18n={i18n}
isMe={false}
title={group.title} title={group.title}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}

View File

@@ -48,7 +48,6 @@ export type PropsDataType = {
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
>; >;
to: Pick< to: Pick<
@@ -183,7 +182,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={from.acceptedMessageRequest}
avatarUrl={from.avatarUrl} avatarUrl={from.avatarUrl}
badge={getPreferredBadge(from.badges)} badge={getPreferredBadge(from.badges)}
color={from.color} color={from.color}
@@ -192,8 +190,8 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
headerName={headerName} headerName={headerName}
i18n={i18n} i18n={i18n}
id={id} id={id}
isNoteToSelf={isNoteToSelf}
isMe={from.isMe} isMe={from.isMe}
isNoteToSelf={isNoteToSelf}
isSelected={false} isSelected={false}
messageText={messageText} messageText={messageText}
onClick={onClickItem} onClick={onClickItem}
@@ -202,7 +200,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
sharedGroupNames={from.sharedGroupNames} sharedGroupNames={from.sharedGroupNames}
theme={theme} theme={theme}
title={from.title} title={from.title}
unblurredAvatarUrl={from.unblurredAvatarUrl}
/> />
); );
} }

View File

@@ -93,11 +93,9 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
const avatar = ( const avatar = (
<Avatar <Avatar
acceptedMessageRequest={false}
color={AvatarColors[0]} color={AvatarColors[0]}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={false}
phoneNumber={phoneNumber.userInput} phoneNumber={phoneNumber.userInput}
title={phoneNumber.userInput} title={phoneNumber.userInput}
sharedGroupNames={[]} sharedGroupNames={[]}

View File

@@ -92,11 +92,9 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
<ListTile <ListTile
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={false}
conversationType="direct" conversationType="direct"
searchResult searchResult
i18n={i18n} i18n={i18n}
isMe={false}
title={phoneNumber.userInput} title={phoneNumber.userInput}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
badge={undefined} badge={undefined}

View File

@@ -66,12 +66,10 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
const avatar = ( const avatar = (
<Avatar <Avatar
acceptedMessageRequest={false}
color={AvatarColors[0]} color={AvatarColors[0]}
conversationType="direct" conversationType="direct"
searchResult searchResult
i18n={i18n} i18n={i18n}
isMe={false}
title={title} title={title}
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}

View File

@@ -61,11 +61,9 @@ export function UsernameSearchResultListItem({
<ListTile <ListTile
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={false}
conversationType="direct" conversationType="direct"
searchResult searchResult
i18n={i18n} i18n={i18n}
isMe={false}
title={username} title={username}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
badge={undefined} badge={undefined}

View File

@@ -214,11 +214,12 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
<ContactPills> <ContactPills>
{this.#selectedContacts.map(contact => ( {this.#selectedContacts.map(contact => (
<ContactPill <ContactPill
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarUrl={contact.avatarUrl} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName} firstName={contact.systemGivenName ?? contact.firstName}
hasAvatar={contact.hasAvatar}
i18n={i18n} i18n={i18n}
id={contact.id} id={contact.id}
isMe={contact.isMe} isMe={contact.isMe}

View File

@@ -104,6 +104,8 @@ import { getProfile } from './util/getProfile';
import { generateMessageId } from './util/generateMessageId'; import { generateMessageId } from './util/generateMessageId';
import { postSaveUpdates } from './util/cleanup'; import { postSaveUpdates } from './util/cleanup';
import { MessageModel } from './models/messages'; import { MessageModel } from './models/messages';
import { areWePending } from './util/groupMembershipUtils';
import { isConversationAccepted } from './util/isConversationAccepted';
type AccessRequiredEnum = Proto.AccessControl.AccessRequired; type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
@@ -3792,11 +3794,11 @@ async function updateGroupViaPreJoinInfo({
newAttributes = { newAttributes = {
...newAttributes, ...newAttributes,
...(await applyNewAvatar( ...(await applyNewAvatar({
dropNull(preJoinInfo.avatar), newAvatarUrl: dropNull(preJoinInfo.avatar),
newAttributes, attributes: newAttributes,
logId logId,
)), })),
}; };
return { return {
@@ -5441,7 +5443,11 @@ async function applyGroupChange({
const { avatar } = actions.modifyAvatar; const { avatar } = actions.modifyAvatar;
result = { result = {
...result, ...result,
...(await applyNewAvatar(dropNull(avatar), result, logId)), ...(await applyNewAvatar({
newAvatarUrl: dropNull(avatar),
attributes: result,
logId,
})),
}; };
} }
@@ -5721,13 +5727,23 @@ export async function decryptGroupAvatar(
// Overwriting result.avatar as part of functionality // Overwriting result.avatar as part of functionality
export async function applyNewAvatar( export async function applyNewAvatar(
newAvatarUrl: string | undefined, options:
attributes: Readonly< | {
Pick<ConversationAttributesType, 'avatar' | 'secretParams'> newAvatarUrl?: string | undefined;
>, attributes: Pick<ConversationAttributesType, 'avatar' | 'secretParams'>;
logId: string logId: string;
): Promise<Pick<ConversationAttributesType, 'avatar'>> { forceDownload: true;
const result: Pick<ConversationAttributesType, 'avatar'> = {}; }
| {
newAvatarUrl?: string | undefined;
attributes: ConversationAttributesType;
logId: string;
forceDownload?: false | undefined;
}
): Promise<Pick<ConversationAttributesType, 'avatar' | 'remoteAvatarUrl'>> {
const { newAvatarUrl, attributes, logId, forceDownload } = options;
const result: Pick<ConversationAttributesType, 'avatar' | 'remoteAvatarUrl'> =
{};
try { try {
// Avatar has been dropped // Avatar has been dropped
if (!newAvatarUrl && attributes.avatar) { if (!newAvatarUrl && attributes.avatar) {
@@ -5739,19 +5755,34 @@ export async function applyNewAvatar(
result.avatar = undefined; result.avatar = undefined;
} }
const avatarUrlToUse =
newAvatarUrl ||
('remoteAvatarUrl' in attributes
? attributes.remoteAvatarUrl
: undefined);
// Group has avatar; has it changed? // Group has avatar; has it changed?
if ( if (
newAvatarUrl && avatarUrlToUse &&
(!attributes.avatar?.path || attributes.avatar.url !== newAvatarUrl) (!attributes.avatar?.path || attributes.avatar.url !== avatarUrlToUse)
) { ) {
if (!attributes.secretParams) { if (!attributes.secretParams) {
throw new Error('applyNewAvatar: group was missing secretParams!'); throw new Error('applyNewAvatar: group was missing secretParams!');
} }
const data = await decryptGroupAvatar( if (
newAvatarUrl, !forceDownload &&
(areWePending(attributes) || !isConversationAccepted(attributes))
) {
result.remoteAvatarUrl = avatarUrlToUse;
return result;
}
const data: Uint8Array = await decryptGroupAvatar(
avatarUrlToUse,
attributes.secretParams attributes.secretParams
); );
const hash = computeHash(data); const hash = computeHash(data);
if (attributes.avatar?.hash === hash) { if (attributes.avatar?.hash === hash) {
@@ -5760,7 +5791,7 @@ export async function applyNewAvatar(
); );
result.avatar = { result.avatar = {
...attributes.avatar, ...attributes.avatar,
url: newAvatarUrl, url: avatarUrlToUse,
}; };
return result; return result;
} }
@@ -5770,10 +5801,9 @@ export async function applyNewAvatar(
attributes.avatar.path attributes.avatar.path
); );
} }
const local = await window.Signal.Migrations.writeNewAttachmentData(data); const local = await window.Signal.Migrations.writeNewAttachmentData(data);
result.avatar = { result.avatar = {
url: newAvatarUrl, url: avatarUrlToUse,
...local, ...local,
hash, hash,
}; };
@@ -5871,7 +5901,11 @@ async function applyGroupState({
// avatar // avatar
result = { result = {
...result, ...result,
...(await applyNewAvatar(dropNull(groupState.avatar), result, logId)), ...(await applyNewAvatar({
newAvatarUrl: dropNull(groupState.avatar),
attributes: result,
logId,
})),
}; };
// disappearingMessagesTimer // disappearingMessagesTimer

View File

@@ -415,7 +415,12 @@ export async function joinViaLink(value: string): Promise<void> {
secretParams, secretParams,
}; };
try { try {
const patch = await applyNewAvatar(result.avatar, attributes, logId); const patch = await applyNewAvatar({
newAvatarUrl: result.avatar,
attributes,
logId,
forceDownload: true,
});
attributes = { ...attributes, ...patch }; attributes = { ...attributes, ...patch };
if (attributes.avatar && attributes.avatar.path) { if (attributes.avatar && attributes.avatar.path) {

View File

@@ -28,6 +28,7 @@ export type MinimalConversation = Satisfies<
| 'color' | 'color'
| 'expireTimer' | 'expireTimer'
| 'groupVersion' | 'groupVersion'
| 'hasAvatar'
| 'id' | 'id'
| 'isArchived' | 'isArchived'
| 'isBlocked' | 'isBlocked'
@@ -43,8 +44,10 @@ export type MinimalConversation = Satisfies<
| 'profileName' | 'profileName'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl' > & {
> gradientStart?: string;
gradientEnd?: string;
}
>; >;
export function useMinimalConversation( export function useMinimalConversation(
@@ -59,6 +62,7 @@ export function useMinimalConversation(
color, color,
expireTimer, expireTimer,
groupVersion, groupVersion,
hasAvatar,
id, id,
isArchived, isArchived,
isBlocked, isBlocked,
@@ -74,10 +78,13 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarUrl,
} = conversation; } = conversation;
return useMemo(() => { const gradientStart = conversation.avatarPlaceholderGradient?.[0];
const gradientEnd = conversation.avatarPlaceholderGradient?.[1];
return useMemo((): MinimalConversation => {
return { return {
gradientStart,
gradientEnd,
acceptedMessageRequest, acceptedMessageRequest,
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
@@ -86,6 +93,7 @@ export function useMinimalConversation(
color, color,
expireTimer, expireTimer,
groupVersion, groupVersion,
hasAvatar,
id, id,
isArchived, isArchived,
isBlocked, isBlocked,
@@ -101,9 +109,10 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarUrl,
}; };
}, [ }, [
gradientStart,
gradientEnd,
acceptedMessageRequest, acceptedMessageRequest,
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
@@ -112,6 +121,7 @@ export function useMinimalConversation(
color, color,
expireTimer, expireTimer,
groupVersion, groupVersion,
hasAvatar,
id, id,
isArchived, isArchived,
isBlocked, isBlocked,
@@ -127,6 +137,5 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarUrl,
]); ]);
} }

View File

@@ -44,7 +44,11 @@ export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
} }
// Generate correct attributes patch // Generate correct attributes patch
const patch = await applyNewAvatar(newAvatarUrl, attributes, logId); const patch = await applyNewAvatar({
newAvatarUrl,
attributes,
logId,
});
convo.set(patch); convo.set(patch);
await DataWriter.updateConversation(convo.attributes); await DataWriter.updateConversation(convo.attributes);

12
ts/model-types.d.ts vendored
View File

@@ -499,18 +499,12 @@ export type ConversationAttributesType = {
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).
unblurredAvatarUrl?: string;
// Legacy field, mapped to above in getConversation() // Legacy field, mapped to above in getConversation()
unblurredAvatarPath?: string; unblurredAvatarPath?: string;
// remoteAvatarUrl
remoteAvatarUrl?: string;
// Only used during backup integration tests. After import, our data model merges // Only used during backup integration tests. After import, our data model merges
// Contact and Chat frames from a backup, and we will then by default export both, even // Contact and Chat frames from a backup, and we will then by default export both, even
// if the Chat frame was not imported. That's fine in normal usage, but breaks // if the Chat frame was not imported. That's fine in normal usage, but breaks

View File

@@ -34,7 +34,6 @@ import {
getAvatar, getAvatar,
getRawAvatarPath, getRawAvatarPath,
getLocalAvatarUrl, getLocalAvatarUrl,
getLocalProfileAvatarUrl,
} from '../util/avatarUtils'; } from '../util/avatarUtils';
import { getDraftPreview } from '../util/getDraftPreview'; import { getDraftPreview } from '../util/getDraftPreview';
import { hasDraft } from '../util/hasDraft'; import { hasDraft } from '../util/hasDraft';
@@ -192,6 +191,7 @@ import { getIsInitialContactSync } from '../services/contactSync';
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads'; import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
import { cleanupMessages } from '../util/cleanup'; import { cleanupMessages } from '../util/cleanup';
import { MessageModel } from './messages'; import { MessageModel } from './messages';
import { applyNewAvatar } from '../groups';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -2485,6 +2485,25 @@ export class ConversationModel extends window.Backbone
// time to go through old messages to download attachments. // time to go through old messages to download attachments.
if (didResponseChange && !wasPreviouslyAccepted) { if (didResponseChange && !wasPreviouslyAccepted) {
await this.handleReadAndDownloadAttachments({ isLocalAction }); await this.handleReadAndDownloadAttachments({ isLocalAction });
if (this.attributes.remoteAvatarUrl) {
if (isDirectConversation(this.attributes)) {
drop(
this.setAndMaybeFetchProfileAvatar({
avatarUrl: this.attributes.remoteAvatarUrl,
})
);
}
if (isGroup(this.attributes)) {
const updateAttrs = await applyNewAvatar({
newAvatarUrl: this.attributes.remoteAvatarUrl,
attributes: this.attributes,
logId: 'applyMessageRequestResponse',
forceDownload: true,
});
this.set(updateAttrs);
}
}
} }
if (isLocalAction) { if (isLocalAction) {
@@ -4922,10 +4941,12 @@ export class ConversationModel extends window.Backbone
} }
} }
async setAndMaybeFetchProfileAvatar( async setAndMaybeFetchProfileAvatar(options: {
avatarUrl: undefined | null | string, avatarUrl: undefined | null | string;
decryptionKey: Uint8Array decryptionKey?: Uint8Array | null | undefined;
): Promise<void> { forceFetch?: boolean;
}): Promise<void> {
const { avatarUrl, decryptionKey, forceFetch } = options;
if (isMe(this.attributes)) { if (isMe(this.attributes)) {
if (avatarUrl) { if (avatarUrl) {
await window.storage.put('avatarUrl', avatarUrl); await window.storage.put('avatarUrl', avatarUrl);
@@ -4944,10 +4965,28 @@ export class ConversationModel extends window.Backbone
throw new Error('setProfileAvatar: Cannot fetch avatar when offline!'); throw new Error('setProfileAvatar: Cannot fetch avatar when offline!');
} }
if (!this.getAccepted({ ignoreEmptyConvo: true }) && !forceFetch) {
this.set({ remoteAvatarUrl: avatarUrl });
return;
}
const avatar = await messaging.getAvatar(avatarUrl); const avatar = await messaging.getAvatar(avatarUrl);
// If decryptionKey isn't provided, use the one from the model
const modelProfileKey = this.get('profileKey');
const updatedDecryptionKey =
decryptionKey ||
(modelProfileKey ? Bytes.fromBase64(modelProfileKey) : null);
if (!updatedDecryptionKey) {
log.warn(
'setAndMaybeFetchProfileAvatar: No decryption key provided and none found on model'
);
return;
}
// decrypt // decrypt
const decrypted = decryptProfile(avatar, decryptionKey); const decrypted = decryptProfile(avatar, updatedDecryptionKey);
// update the conversation avatar only if hash differs // update the conversation avatar only if hash differs
if (decrypted) { if (decrypted) {
@@ -5303,15 +5342,6 @@ export class ConversationModel extends window.Backbone
}; };
} }
unblurAvatar(): void {
const avatarUrl = getLocalProfileAvatarUrl(this.attributes);
if (avatarUrl) {
this.set('unblurredAvatarUrl', avatarUrl);
} else {
this.unset('unblurredAvatarUrl');
}
}
areWeAdmin(): boolean { areWeAdmin(): boolean {
return areWeAdmin(this.attributes); return areWeAdmin(this.attributes);
} }

View File

@@ -294,17 +294,16 @@ export class MentionCompletion {
)} )}
> >
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} avatarPlaceholderGradient={member.avatarPlaceholderGradient}
avatarUrl={member.avatarUrl} avatarUrl={member.avatarUrl}
badge={getPreferredBadge(member.badges)} badge={getPreferredBadge(member.badges)}
conversationType="direct" conversationType="direct"
hasAvatar={member.hasAvatar}
i18n={this.options.i18n} i18n={this.options.i18n}
isMe={member.isMe}
sharedGroupNames={member.sharedGroupNames} sharedGroupNames={member.sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
theme={theme} theme={theme}
title={member.title} title={member.title}
unblurredAvatarUrl={member.unblurredAvatarUrl}
/> />
<div className="module-composition-input__suggestions__title"> <div className="module-composition-input__suggestions__title">
<UserText text={member.title} /> <UserText text={member.title} />

View File

@@ -784,10 +784,10 @@ async function doGetProfile(
try { try {
if (requestDecryptionKey != null) { if (requestDecryptionKey != null) {
// Note: Fetches avatar // Note: Fetches avatar
await c.setAndMaybeFetchProfileAvatar( await c.setAndMaybeFetchProfileAvatar({
profile.avatar, avatarUrl: profile.avatar,
requestDecryptionKey decryptionKey: requestDecryptionKey,
); });
} }
} catch (error) { } catch (error) {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {

View File

@@ -1743,7 +1743,10 @@ export async function mergeAccountRecord(
); );
const avatarUrl = dropNull(accountRecord.avatarUrl); const avatarUrl = dropNull(accountRecord.avatarUrl);
await conversation.setAndMaybeFetchProfileAvatar(avatarUrl, profileKey); await conversation.setAndMaybeFetchProfileAvatar({
avatarUrl,
decryptionKey: profileKey,
});
await window.storage.put('avatarUrl', avatarUrl); await window.storage.put('avatarUrl', avatarUrl);
} }

View File

@@ -1655,9 +1655,7 @@ function saveConversation(db: WritableDB, data: ConversationType): void {
` `
).run({ ).run({
id, id,
json: objectToJSON( json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
omit(data, ['profileLastFetchedAt', 'unblurredAvatarUrl'])
),
e164: e164 || null, e164: e164 || null,
serviceId: serviceId || null, serviceId: serviceId || null,
@@ -1723,9 +1721,7 @@ function updateConversation(db: WritableDB, data: ConversationType): void {
` `
).run({ ).run({
id, id,
json: objectToJSON( json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
omit(data, ['profileLastFetchedAt', 'unblurredAvatarUrl'])
),
e164: e164 || null, e164: e164 || null,
serviceId: serviceId || null, serviceId: serviceId || null,

View File

@@ -97,6 +97,7 @@ import {
getConversationSelector, getConversationSelector,
getMe, getMe,
getMessagesByConversation, getMessagesByConversation,
getPendingAvatarDownloadSelector,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import type { import type {
@@ -302,8 +303,9 @@ export type ConversationType = ReadonlyDeep<
avatarUrl?: string; avatarUrl?: string;
rawAvatarPath?: string; rawAvatarPath?: string;
avatarHash?: string; avatarHash?: string;
avatarPlaceholderGradient?: Readonly<[string, string]>;
profileAvatarUrl?: string; profileAvatarUrl?: string;
unblurredAvatarUrl?: string; hasAvatar?: boolean;
areWeAdmin?: boolean; areWeAdmin?: boolean;
areWePending?: boolean; areWePending?: boolean;
areWePendingApproval?: boolean; areWePendingApproval?: boolean;
@@ -578,6 +580,10 @@ export type ConversationsStateType = ReadonlyDeep<{
messagesLookup: MessageLookupType; messagesLookup: MessageLookupType;
messagesByConversation: MessagesByConversationType; messagesByConversation: MessagesByConversationType;
// Map of conversation IDs to a boolean indicating whether an avatar download
// was requested
pendingRequestedAvatarDownload: Record<string, boolean>;
preloadData?: ConversationPreloadDataType; preloadData?: ConversationPreloadDataType;
}>; }>;
@@ -640,6 +646,8 @@ export const SET_VOICE_NOTE_PLAYBACK_RATE =
'conversations/SET_VOICE_NOTE_PLAYBACK_RATE'; 'conversations/SET_VOICE_NOTE_PLAYBACK_RATE';
export const CONVERSATION_UNLOADED = 'CONVERSATION_UNLOADED'; export const CONVERSATION_UNLOADED = 'CONVERSATION_UNLOADED';
export const SHOW_SPOILER = 'conversations/SHOW_SPOILER'; export const SHOW_SPOILER = 'conversations/SHOW_SPOILER';
export const SET_PENDING_REQUESTED_AVATAR_DOWNLOAD =
'conversations/SET_PENDING_REQUESTED_AVATAR_DOWNLOAD';
export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{ export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION; type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
@@ -830,6 +838,14 @@ export type ShowSpoilerActionType = ReadonlyDeep<{
}; };
}>; }>;
export type SetPendingRequestedAvatarDownloadActionType = ReadonlyDeep<{
type: typeof SET_PENDING_REQUESTED_AVATAR_DOWNLOAD;
payload: {
conversationId: string;
value: boolean;
};
}>;
export type MessagesAddedActionType = ReadonlyDeep<{ export type MessagesAddedActionType = ReadonlyDeep<{
type: 'MESSAGES_ADDED'; type: 'MESSAGES_ADDED';
payload: { payload: {
@@ -1064,6 +1080,7 @@ export type ConversationActionType =
| ReplaceAvatarsActionType | ReplaceAvatarsActionType
| ReviewConversationNameCollisionActionType | ReviewConversationNameCollisionActionType
| ScrollToMessageActionType | ScrollToMessageActionType
| SetPendingRequestedAvatarDownloadActionType
| TargetedConversationChangedActionType | TargetedConversationChangedActionType
| SetComposeGroupAvatarActionType | SetComposeGroupAvatarActionType
| SetComposeGroupExpireTimerActionType | SetComposeGroupExpireTimerActionType
@@ -1179,6 +1196,8 @@ export const actions = {
saveAvatarToDisk, saveAvatarToDisk,
scrollToMessage, scrollToMessage,
scrollToOldestUnreadMention, scrollToOldestUnreadMention,
setPendingRequestedAvatarDownload,
startAvatarDownload,
showSpoiler, showSpoiler,
targetMessage, targetMessage,
setAccessControlAddFromInviteLinkSetting, setAccessControlAddFromInviteLinkSetting,
@@ -1220,7 +1239,6 @@ export const actions = {
toggleHideStories, toggleHideStories,
toggleSelectMessage, toggleSelectMessage,
toggleSelectMode, toggleSelectMode,
unblurAvatar,
updateConversationModelSharedGroups, updateConversationModelSharedGroups,
updateGroupAttributes, updateGroupAttributes,
updateLastMessage, updateLastMessage,
@@ -1465,19 +1483,7 @@ function removeMember(
payload: null, payload: null,
}; };
} }
function unblurAvatar(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('unblurAvatar: Conversation not found!');
}
conversation.unblurAvatar();
return {
type: 'NOOP',
payload: null,
};
}
function updateSharedGroups(conversationId: string): NoopActionType { function updateSharedGroups(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
@@ -4873,6 +4879,73 @@ function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
}; };
} }
function setPendingRequestedAvatarDownload(
conversationId: string,
value: boolean
): SetPendingRequestedAvatarDownloadActionType {
return {
type: SET_PENDING_REQUESTED_AVATAR_DOWNLOAD,
payload: {
conversationId,
value,
},
};
}
function startAvatarDownload(
conversationId: string
): ThunkAction<
void,
RootStateType,
unknown,
SetPendingRequestedAvatarDownloadActionType
> {
return async (dispatch, getState) => {
const isAlreadyLoading =
getPendingAvatarDownloadSelector(getState())(conversationId);
if (isAlreadyLoading) {
return;
}
dispatch(setPendingRequestedAvatarDownload(conversationId, true));
try {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('startAvatarDownload: Conversation not found');
}
if (!conversation.attributes.remoteAvatarUrl) {
throw new Error('startAvatarDownload: no avatar URL');
}
if (isGroup(conversation.attributes)) {
const updateAttrs = await groups.applyNewAvatar({
newAvatarUrl: conversation.attributes.remoteAvatarUrl,
attributes: conversation.attributes,
logId: 'startAvatarDownload',
forceDownload: true,
});
conversation.set(updateAttrs);
} else {
await conversation.setAndMaybeFetchProfileAvatar({
avatarUrl: conversation.attributes.remoteAvatarUrl,
decryptionKey: null,
forceFetch: true,
});
}
await DataWriter.updateConversation(conversation.attributes);
} catch (error) {
log.error(
'startAvatarDownload: Failed to download avatar',
Errors.toLogFormat(error)
);
} finally {
dispatch(setPendingRequestedAvatarDownload(conversationId, false));
}
};
}
// Reducer // Reducer
export function getEmptyState(): ConversationsStateType { export function getEmptyState(): ConversationsStateType {
@@ -4892,6 +4965,7 @@ export function getEmptyState(): ConversationsStateType {
selectedMessageIds: undefined, selectedMessageIds: undefined,
showArchived: false, showArchived: false,
hasContactSpoofingReview: false, hasContactSpoofingReview: false,
pendingRequestedAvatarDownload: {},
targetedConversationPanels: { targetedConversationPanels: {
isAnimating: false, isAnimating: false,
wasAnimated: false, wasAnimated: false,
@@ -7232,5 +7306,17 @@ export function reducer(
}; };
} }
if (action.type === SET_PENDING_REQUESTED_AVATAR_DOWNLOAD) {
const { conversationId, value } = action.payload;
return {
...state,
pendingRequestedAvatarDownload: {
...state.pendingRequestedAvatarDownload,
[conversationId]: value,
},
};
}
return state; return state;
} }

View File

@@ -1339,3 +1339,14 @@ export const getPreloadedConversationId = createSelector(
getConversations, getConversations,
({ preloadData }): string | undefined => preloadData?.conversationId ({ preloadData }): string | undefined => preloadData?.conversationId
); );
export const getPendingAvatarDownloadSelector = createSelector(
getConversations,
(conversations: ConversationsStateType) => {
return (conversationId: string): boolean => {
return Boolean(
conversations.pendingRequestedAvatarDownload[conversationId]
);
};
}
);

View File

@@ -164,7 +164,6 @@ type FormattedContact = Partial<ConversationType> &
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarUrl'
>; >;
export type PropsForMessage = Omit<TimelineMessagePropsData, 'interactionMode'>; export type PropsForMessage = Omit<TimelineMessagePropsData, 'interactionMode'>;
export type MessagePropsType = Omit< export type MessagePropsType = Omit<
@@ -351,11 +350,13 @@ const getAuthorForMessage = (
options: GetContactOptions options: GetContactOptions
): PropsData['author'] => { ): PropsData['author'] => {
const { const {
avatarPlaceholderGradient,
acceptedMessageRequest, acceptedMessageRequest,
avatarUrl, avatarUrl,
badges, badges,
color, color,
firstName, firstName,
hasAvatar,
id, id,
isMe, isMe,
name, name,
@@ -363,15 +364,16 @@ const getAuthorForMessage = (
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarUrl,
} = getContact(message, options); } = getContact(message, options);
const unsafe = { const unsafe = {
avatarPlaceholderGradient,
acceptedMessageRequest, acceptedMessageRequest,
avatarUrl, avatarUrl,
badges, badges,
color, color,
firstName, firstName,
hasAvatar,
id, id,
isMe, isMe,
name, name,
@@ -379,7 +381,6 @@ const getAuthorForMessage = (
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarUrl,
}; };
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe; const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;

View File

@@ -166,11 +166,13 @@ export function getStoryView(
const sender = pick( const sender = pick(
conversationSelector(story.sourceServiceId || story.source), conversationSelector(story.sourceServiceId || story.source),
[ [
'avatarPlaceholderGradient',
'acceptedMessageRequest', 'acceptedMessageRequest',
'avatarUrl', 'avatarUrl',
'badges', 'badges',
'color', 'color',
'firstName', 'firstName',
'hasAvatar',
'hideStory', 'hideStory',
'id', 'id',
'isMe', 'isMe',

View File

@@ -6,7 +6,10 @@ import { AboutContactModal } from '../../components/conversation/AboutContactMod
import { isSignalConnection } from '../../util/getSignalConnections'; import { isSignalConnection } from '../../util/getSignalConnections';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getGlobalModalsState } from '../selectors/globalModals'; import { getGlobalModalsState } from '../selectors/globalModals';
import { getConversationSelector } from '../selectors/conversations'; import {
getConversationSelector,
getPendingAvatarDownloadSelector,
} from '../selectors/conversations';
import type { ConversationType } from '../ducks/conversations'; import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
@@ -35,8 +38,9 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
const globalModals = useSelector(getGlobalModalsState); const globalModals = useSelector(getGlobalModalsState);
const { aboutContactModalContactId: contactId } = globalModals; const { aboutContactModalContactId: contactId } = globalModals;
const getConversation = useSelector(getConversationSelector); const getConversation = useSelector(getConversationSelector);
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
const { updateSharedGroups, unblurAvatar } = useConversationsActions(); const { startAvatarDownload, updateSharedGroups } = useConversationsActions();
const conversation = getConversation(contactId); const conversation = getConversation(contactId);
const { id: conversationId } = conversation ?? {}; const { id: conversationId } = conversation ?? {};
@@ -63,7 +67,6 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
i18n={i18n} i18n={i18n}
conversation={conversation} conversation={conversation}
updateSharedGroups={updateSharedGroups} updateSharedGroups={updateSharedGroups}
unblurAvatar={unblurAvatar}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
toggleSafetyNumberModal={toggleSafetyNumberModal} toggleSafetyNumberModal={toggleSafetyNumberModal}
isSignalConnection={isSignalConnection(conversation)} isSignalConnection={isSignalConnection(conversation)}
@@ -71,6 +74,12 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
onClose={toggleAboutContactModal} onClose={toggleAboutContactModal}
onOpenNotePreviewModal={handleOpenNotePreviewModal} onOpenNotePreviewModal={handleOpenNotePreviewModal}
toggleProfileNameWarningModal={toggleProfileNameWarningModal} toggleProfileNameWarningModal={toggleProfileNameWarningModal}
pendingAvatarDownload={
conversationId ? isPendingAvatarDownload(conversationId) : false
}
startAvatarDownload={
conversationId ? () => startAvatarDownload(conversationId) : undefined
}
/> />
); );
}); });

View File

@@ -54,6 +54,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
updateConversationModelSharedGroups, updateConversationModelSharedGroups,
toggleAdmin, toggleAdmin,
blockConversation, blockConversation,
startAvatarDownload,
} = useConversationsActions(); } = useConversationsActions();
const { viewUserStories } = useStoriesActions(); const { viewUserStories } = useStoriesActions();
const { const {
@@ -94,6 +95,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation} onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
removeMemberFromGroup={removeMemberFromGroup} removeMemberFromGroup={removeMemberFromGroup}
showConversation={showConversation} showConversation={showConversation}
startAvatarDownload={() => startAvatarDownload(contact.id)}
theme={theme} theme={theme}
toggleAboutContactModal={toggleAboutContactModal} toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal} toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}

View File

@@ -23,6 +23,7 @@ import {
getAllComposableConversations, getAllComposableConversations,
getConversationByIdSelector, getConversationByIdSelector,
getConversationByServiceIdSelector, getConversationByServiceIdSelector,
getPendingAvatarDownloadSelector,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { import {
getAreWeASubscriber, getAreWeASubscriber,
@@ -98,6 +99,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
const conversationSelector = useSelector(getConversationByIdSelector); const conversationSelector = useSelector(getConversationByIdSelector);
const defaultConversationColor = useSelector(getDefaultConversationColor); const defaultConversationColor = useSelector(getDefaultConversationColor);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
const selectedNavTab = useSelector(getSelectedNavTab); const selectedNavTab = useSelector(getSelectedNavTab);
const { const {
@@ -114,6 +116,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
setDisappearingMessages, setDisappearingMessages,
setMuteExpiration, setMuteExpiration,
showConversation, showConversation,
startAvatarDownload,
updateGroupAttributes, updateGroupAttributes,
updateNicknameAndNote, updateNicknameAndNote,
} = useConversationsActions(); } = useConversationsActions();
@@ -205,6 +208,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation} onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation} onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
pendingApprovalMemberships={pendingApprovalMemberships} pendingApprovalMemberships={pendingApprovalMemberships}
pendingAvatarDownload={isPendingAvatarDownload(conversationId)}
pendingMemberships={pendingMemberships} pendingMemberships={pendingMemberships}
pushPanelForConversation={pushPanelForConversation} pushPanelForConversation={pushPanelForConversation}
renderChooseGroupMembersModal={renderChooseGroupMembersModal} renderChooseGroupMembersModal={renderChooseGroupMembersModal}
@@ -218,6 +222,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
showContactModal={showContactModal} showContactModal={showContactModal}
showConversation={showConversation} showConversation={showConversation}
showLightbox={showLightbox} showLightbox={showLightbox}
startAvatarDownload={() => startAvatarDownload(conversationId)}
theme={theme} theme={theme}
toggleAboutContactModal={toggleAboutContactModal} toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal} toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}

View File

@@ -8,7 +8,10 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories2'; import { getHasStoriesSelector } from '../selectors/stories2';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
import { getConversationSelector } from '../selectors/conversations'; import {
getConversationSelector,
getPendingAvatarDownloadSelector,
} from '../selectors/conversations';
import { import {
type ConversationType, type ConversationType,
useConversationsActions, useConversationsActions,
@@ -46,6 +49,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector); const hasStoriesSelector = useSelector(getHasStoriesSelector);
const conversationSelector = useSelector(getConversationSelector); const conversationSelector = useSelector(getConversationSelector);
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
const conversation = conversationSelector(id); const conversation = conversationSelector(id);
if (conversation == null) { if (conversation == null) {
throw new Error(`Did not find conversation ${id} in state!`); throw new Error(`Did not find conversation ${id} in state!`);
@@ -55,7 +59,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
const isSignalConversationValue = isSignalConversation(conversation); const isSignalConversationValue = isSignalConversation(conversation);
const fromOrAddedByTrustedContact = const fromOrAddedByTrustedContact =
isFromOrAddedByTrustedContact(conversation); isFromOrAddedByTrustedContact(conversation);
const { pushPanelForConversation, unblurAvatar, updateSharedGroups } = const { pushPanelForConversation, startAvatarDownload, updateSharedGroups } =
useConversationsActions(); useConversationsActions();
const { toggleAboutContactModal, toggleProfileNameWarningModal } = const { toggleAboutContactModal, toggleProfileNameWarningModal } =
useGlobalModalActions(); useGlobalModalActions();
@@ -64,10 +68,12 @@ export const SmartHeroRow = memo(function SmartHeroRow({
}, [pushPanelForConversation]); }, [pushPanelForConversation]);
const { viewUserStories } = useStoriesActions(); const { viewUserStories } = useStoriesActions();
const { const {
avatarPlaceholderGradient,
about, about,
acceptedMessageRequest, acceptedMessageRequest,
avatarUrl, avatarUrl,
groupDescription, groupDescription,
hasAvatar,
isMe, isMe,
membersCount, membersCount,
nicknameGivenName, nicknameGivenName,
@@ -77,7 +83,6 @@ export const SmartHeroRow = memo(function SmartHeroRow({
sharedGroupNames, sharedGroupNames,
title, title,
type, type,
unblurredAvatarUrl,
} = conversation; } = conversation;
const isDirectConvoAndHasNickname = const isDirectConvoAndHasNickname =
@@ -85,6 +90,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
return ( return (
<ConversationHero <ConversationHero
avatarPlaceholderGradient={avatarPlaceholderGradient}
about={about} about={about}
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
@@ -92,6 +98,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
conversationType={type} conversationType={type}
fromOrAddedByTrustedContact={fromOrAddedByTrustedContact} fromOrAddedByTrustedContact={fromOrAddedByTrustedContact}
groupDescription={groupDescription} groupDescription={groupDescription}
hasAvatar={hasAvatar}
hasStories={hasStories} hasStories={hasStories}
i18n={i18n} i18n={i18n}
id={id} id={id}
@@ -100,15 +107,15 @@ export const SmartHeroRow = memo(function SmartHeroRow({
isSignalConversation={isSignalConversationValue} isSignalConversation={isSignalConversationValue}
membersCount={membersCount} membersCount={membersCount}
openConversationDetails={openConversationDetails} openConversationDetails={openConversationDetails}
pendingAvatarDownload={isPendingAvatarDownload(id)}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
startAvatarDownload={() => startAvatarDownload(id)}
theme={theme} theme={theme}
title={title} title={title}
toggleAboutContactModal={toggleAboutContactModal} toggleAboutContactModal={toggleAboutContactModal}
toggleProfileNameWarningModal={toggleProfileNameWarningModal} toggleProfileNameWarningModal={toggleProfileNameWarningModal}
unblurAvatar={unblurAvatar}
unblurredAvatarUrl={unblurredAvatarUrl}
updateSharedGroups={updateSharedGroups} updateSharedGroups={updateSharedGroups}
viewUserStories={viewUserStories} viewUserStories={viewUserStories}
/> />

View File

@@ -12,6 +12,7 @@ import type { GroupListItemConversationType } from '../../components/conversatio
import { getRandomColor } from './getRandomColor'; import { getRandomColor } from './getRandomColor';
import { ConversationColors } from '../../types/Colors'; import { ConversationColors } from '../../types/Colors';
import { StorySendMode } from '../../types/Stories'; import { StorySendMode } from '../../types/Stories';
import { getAvatarPlaceholderGradient } from '../../utils/getAvatarPlaceholderGradient';
export const getAvatarPath = (): string => export const getAvatarPath = (): string =>
sample([ sample([
@@ -27,6 +28,7 @@ export function getDefaultConversation(
const lastName = casual.last_name; const lastName = casual.last_name;
return { return {
avatarPlaceholderGradient: getAvatarPlaceholderGradient(0),
acceptedMessageRequest: true, acceptedMessageRequest: true,
avatarUrl: getAvatarPath(), avatarUrl: getAvatarPath(),
badges: [], badges: [],

View File

@@ -1,97 +0,0 @@
// 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,
avatarUrl: '/path/to/avatar.jpg',
sharedGroupNames: [],
unblurredAvatarUrl: undefined,
})
);
});
it('returns false if the message request has been accepted', () => {
assert.isFalse(
shouldBlurAvatar({
acceptedMessageRequest: true,
avatarUrl: '/path/to/avatar.jpg',
isMe: false,
sharedGroupNames: [],
unblurredAvatarUrl: undefined,
})
);
});
it('returns false if there are any shared groups', () => {
assert.isFalse(
shouldBlurAvatar({
sharedGroupNames: ['Tahoe Trip'],
acceptedMessageRequest: false,
avatarUrl: '/path/to/avatar.jpg',
isMe: false,
unblurredAvatarUrl: undefined,
})
);
});
it('returns false if there is no avatar', () => {
assert.isFalse(
shouldBlurAvatar({
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
unblurredAvatarUrl: undefined,
})
);
assert.isFalse(
shouldBlurAvatar({
avatarUrl: undefined,
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
unblurredAvatarUrl: undefined,
})
);
assert.isFalse(
shouldBlurAvatar({
avatarUrl: undefined,
unblurredAvatarUrl: '/some/other/path',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
it('returns false if the avatar was unblurred', () => {
assert.isFalse(
shouldBlurAvatar({
avatarUrl: '/path/to/avatar.jpg',
unblurredAvatarUrl: '/path/to/avatar.jpg',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
it('returns true if the stars align (i.e., not everything above)', () => {
assert.isTrue(
shouldBlurAvatar({
avatarUrl: '/path/to/avatar.jpg',
unblurredAvatarUrl: '/different/path.jpg',
acceptedMessageRequest: false,
isMe: false,
sharedGroupNames: [],
})
);
});
});

View File

@@ -6,7 +6,11 @@ import { assert } from 'chai';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import type { App, Bootstrap } from './fixtures'; import type { App, Bootstrap } from './fixtures';
import { initStorage, debug } from './fixtures'; import { initStorage, debug } from './fixtures';
import { typeIntoInput, waitForEnabledComposer } from '../helpers'; import {
acceptConversation,
typeIntoInput,
waitForEnabledComposer,
} from '../helpers';
describe('storage service', function (this: Mocha.Suite) { describe('storage service', function (this: Mocha.Suite) {
this.timeout(durations.MINUTE); this.timeout(durations.MINUTE);
@@ -50,7 +54,6 @@ describe('storage service', function (this: Mocha.Suite) {
const window = await app.getWindow(); const window = await app.getWindow();
const leftPane = window.locator('#LeftPane'); const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Opening conversation with a stranger'); debug('Opening conversation with a stranger');
debug(stranger.toContact().aci); debug(stranger.toContact().aci);
@@ -77,14 +80,7 @@ describe('storage service', function (this: Mocha.Suite) {
} }
debug('Accept conversation from a stranger'); debug('Accept conversation from a stranger');
await conversationStack await acceptConversation(window);
.locator('.module-message-request-actions button >> "Accept"')
.click();
await window
.locator('.MessageRequestActionsConfirmation')
.getByRole('button', { name: 'Accept' })
.click();
debug('Verify that storage state was updated'); debug('Verify that storage state was updated');
{ {

View File

@@ -233,6 +233,8 @@ export type StorageAccessType = {
postRegistrationSyncsStatus: 'incomplete' | 'complete'; postRegistrationSyncsStatus: 'incomplete' | 'complete';
avatarsHaveBeenMigrated: boolean;
// Deprecated // Deprecated
'challenge:retry-message-ids': never; 'challenge:retry-message-ids': never;
nextSignedKeyRotationTime: number; nextSignedKeyRotationTime: number;

View File

@@ -82,11 +82,13 @@ export type StoryViewType = {
readAt?: number; readAt?: number;
sender: Pick< sender: Pick<
ConversationType, ConversationType,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarUrl' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'firstName' | 'firstName'
| 'hasAvatar'
| 'hideStory' | 'hideStory'
| 'id' | 'id'
| 'isMe' | 'isMe'

View File

@@ -7,6 +7,14 @@ import { isMe } from './whatTypeOfConversation';
import { isSignalConversation } from './isSignalConversation'; import { isSignalConversation } from './isSignalConversation';
import { getLocalAttachmentUrl } from './getLocalAttachmentUrl'; import { getLocalAttachmentUrl } from './getLocalAttachmentUrl';
export function hasAvatar(
conversationAttrs: ConversationAttributesType
): boolean {
return Boolean(
getAvatar(conversationAttrs) || conversationAttrs.remoteAvatarUrl
);
}
export function getAvatarHash( export function getAvatarHash(
conversationAttrs: ConversationAttributesType conversationAttrs: ConversationAttributesType
): undefined | string { ): undefined | string {
@@ -65,29 +73,3 @@ export function getLocalProfileAvatarUrl(
const avatar = conversationAttrs.profileAvatar || conversationAttrs.avatar; const avatar = conversationAttrs.profileAvatar || conversationAttrs.avatar;
return avatar?.path ? getLocalAttachmentUrl(avatar) : undefined; return avatar?.path ? getLocalAttachmentUrl(avatar) : undefined;
} }
export function getLocalUnblurredAvatarUrl(
conversationAttrs: ConversationAttributesType
): string | undefined {
const { unblurredAvatarPath, unblurredAvatarUrl } = conversationAttrs;
if (unblurredAvatarUrl != null) {
return unblurredAvatarUrl;
}
if (unblurredAvatarPath == null) {
return undefined;
}
// Compatibility mode
const avatar = getAvatar(conversationAttrs);
// Since we use `unblurredAvatarUrl` only for equality checks - if the path
// is the same - return equivalent url
if (avatar?.path === unblurredAvatarPath) {
return getLocalAvatarUrl(conversationAttrs);
}
// Otherwise generate some valid url, but it will never be the same because of
// absent "size".
return getLocalAttachmentUrl({ path: unblurredAvatarPath });
}

View File

@@ -17,11 +17,11 @@ import { canEditGroupInfo } from './canEditGroupInfo';
import { dropNull } from './dropNull'; import { dropNull } from './dropNull';
import { getAboutText } from './getAboutText'; import { getAboutText } from './getAboutText';
import { import {
getLocalUnblurredAvatarUrl,
getAvatarHash, getAvatarHash,
getLocalAvatarUrl, getLocalAvatarUrl,
getLocalProfileAvatarUrl, getLocalProfileAvatarUrl,
getRawAvatarPath, getRawAvatarPath,
hasAvatar,
} from './avatarUtils'; } from './avatarUtils';
import { getAvatarData } from './getAvatarData'; import { getAvatarData } from './getAvatarData';
import { getConversationMembers } from './getConversationMembers'; import { getConversationMembers } from './getConversationMembers';
@@ -46,16 +46,17 @@ import {
isMe, isMe,
} from './whatTypeOfConversation'; } from './whatTypeOfConversation';
import { import {
areWePending,
getBannedMemberships, getBannedMemberships,
getMembersCount, getMembersCount,
getMemberships, getMemberships,
getPendingApprovalMemberships, getPendingApprovalMemberships,
getPendingMemberships, getPendingMemberships,
isMember,
isMemberAwaitingApproval, isMemberAwaitingApproval,
isMemberPending,
} from './groupMembershipUtils'; } from './groupMembershipUtils';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import { getIdentifierHash } from '../Crypto';
import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient';
const EMPTY_ARRAY: Readonly<[]> = []; const EMPTY_ARRAY: Readonly<[]> = [];
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {}; const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
@@ -88,7 +89,13 @@ export function getConversation(model: ConversationModel): ConversationType {
); );
const ourAci = window.textsecure.storage.user.getAci(); const ourAci = window.textsecure.storage.user.getAci();
const ourPni = window.textsecure.storage.user.getPni();
const identifierHash = getIdentifierHash({
aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
e164: attributes.e164,
pni: attributes.pni,
groupId: attributes.groupId,
});
const color = migrateColor(attributes.color, { const color = migrateColor(attributes.color, {
aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined, aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
@@ -97,6 +104,11 @@ export function getConversation(model: ConversationModel): ConversationType {
groupId: attributes.groupId, groupId: attributes.groupId,
}); });
const avatarPlaceholderGradient =
hasAvatar(attributes) && identifierHash != null
? getAvatarPlaceholderGradient(identifierHash)
: undefined;
const { draftTimestamp, draftEditMessage, timestamp } = attributes; const { draftTimestamp, draftEditMessage, timestamp } = attributes;
const draftPreview = getDraftPreview(attributes); const draftPreview = getDraftPreview(attributes);
const draftText = dropNull(attributes.draft); const draftText = dropNull(attributes.draft);
@@ -142,20 +154,14 @@ export function getConversation(model: ConversationModel): ConversationType {
acceptedMessageRequest: isConversationAccepted(attributes), acceptedMessageRequest: isConversationAccepted(attributes),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
activeAt: attributes.active_at!, activeAt: attributes.active_at!,
areWePending: areWePending: areWePending(attributes),
ourAci &&
(isMemberPending(attributes, ourAci) ||
Boolean(
ourPni &&
!isMember(attributes, ourAci) &&
isMemberPending(attributes, ourPni)
)),
areWePendingApproval: Boolean( areWePendingApproval: Boolean(
ourConversationId && ourConversationId &&
ourAci && ourAci &&
isMemberAwaitingApproval(attributes, ourAci) isMemberAwaitingApproval(attributes, ourAci)
), ),
areWeAdmin: areWeAdmin(attributes), areWeAdmin: areWeAdmin(attributes),
avatarPlaceholderGradient,
avatars: getAvatarData(attributes), avatars: getAvatarData(attributes),
badges: attributes.badges ?? EMPTY_ARRAY, badges: attributes.badges ?? EMPTY_ARRAY,
canChangeTimer: canChangeTimer(attributes), canChangeTimer: canChangeTimer(attributes),
@@ -164,8 +170,8 @@ export function getConversation(model: ConversationModel): ConversationType {
avatarUrl: getLocalAvatarUrl(attributes), avatarUrl: getLocalAvatarUrl(attributes),
rawAvatarPath: getRawAvatarPath(attributes), rawAvatarPath: getRawAvatarPath(attributes),
avatarHash: getAvatarHash(attributes), avatarHash: getAvatarHash(attributes),
unblurredAvatarUrl: getLocalUnblurredAvatarUrl(attributes),
profileAvatarUrl: getLocalProfileAvatarUrl(attributes), profileAvatarUrl: getLocalProfileAvatarUrl(attributes),
hasAvatar: hasAvatar(attributes),
color, color,
conversationColor: attributes.conversationColor, conversationColor: attributes.conversationColor,
customColor, customColor,

View File

@@ -184,3 +184,22 @@ export function getMemberships(
aci: member.aci, aci: member.aci,
})); }));
} }
export function areWePending(
conversationAttrs: Pick<
ConversationAttributesType,
'groupId' | 'groupVersion' | 'pendingMembersV2'
>
): boolean {
const ourAci = window.textsecure.storage.user.getAci();
const ourPni = window.textsecure.storage.user.getPni();
return Boolean(
ourAci &&
(isMemberPending(conversationAttrs, ourAci) ||
Boolean(
ourPni &&
!isMember(conversationAttrs, ourAci) &&
isMemberPending(conversationAttrs, ourPni)
))
);
}

View File

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

View File

@@ -0,0 +1,33 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const GRADIENTS = [
['#252568', '#9C8F8F'],
['#2A4275', '#9D9EA1'],
['#2E4B5F', '#8AA9B1'],
['#2E426C', '#7A9377'],
['#1A341A', '#807F6E'],
['#464E42', '#D5C38F'],
['#595643', '#93A899'],
['#2C2F36', '#687466'],
['#2B1E18', '#968980'],
['#7B7067', '#A5A893'],
['#706359', '#BDA194'],
['#383331', '#A48788'],
['#924F4F', '#897A7A'],
['#663434', '#C58D77'],
['#8F4B02', '#AA9274'],
['#784747', '#8C8F6F'],
['#747474', '#ACACAC'],
['#49484C', '#A5A6B5'],
['#4A4E4D', '#ABAFAE'],
['#3A3A3A', '#929887'],
] as const;
export function getAvatarPlaceholderGradient(
identifierHash: number
): Readonly<[string, string]> {
const colorIndex = identifierHash % GRADIENTS.length;
return GRADIENTS[colorIndex];
}