Click to download avatar for unaccepted conversations
This commit is contained in:
@@ -21,7 +21,7 @@ import { getAuthorId } from './messages/helpers';
|
||||
import { maybeDeriveGroupV2Id } from './groups';
|
||||
import { assertDev, strictAssert } from './util/assert';
|
||||
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 {
|
||||
isServiceIdString,
|
||||
@@ -39,6 +39,8 @@ import * as StorageService from './services/storage';
|
||||
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats';
|
||||
import { countAllConversationsUnreadStats } from './util/countUnreadStats';
|
||||
import { isTestOrMockEnvironment } from './environment';
|
||||
import { isConversationAccepted } from './util/isConversationAccepted';
|
||||
import { areWePending } from './util/groupMembershipUtils';
|
||||
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
||||
|
||||
type ConvoMatchType =
|
||||
@@ -1372,6 +1374,52 @@ export class ConversationController {
|
||||
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 {
|
||||
const pinnedIds = window.storage.get('pinnedConversationIds', []);
|
||||
|
||||
|
44
ts/Crypto.ts
44
ts/Crypto.ts
@@ -670,6 +670,34 @@ export function constantTimeEqual(
|
||||
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({
|
||||
aci,
|
||||
e164,
|
||||
@@ -681,19 +709,11 @@ export function generateAvatarColor({
|
||||
pni: PniString | undefined;
|
||||
groupId: string | undefined;
|
||||
}): string {
|
||||
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 {
|
||||
const hashValue = getIdentifierHash({ aci, e164, pni, groupId });
|
||||
|
||||
if (hashValue == null) {
|
||||
return sample(AvatarColors) || AvatarColors[0];
|
||||
}
|
||||
|
||||
const digest = hash(HashType.size256, identifier);
|
||||
return AvatarColors[digest[0] % AVATAR_COLOR_COUNT];
|
||||
return AvatarColors[hashValue % AVATAR_COLOR_COUNT];
|
||||
}
|
||||
|
@@ -1445,6 +1445,10 @@ export async function startApp(): Promise<void> {
|
||||
if (window.isBeforeVersion(lastVersion, 'v5.31.0')) {
|
||||
window.ConversationController.repairPinnedConversations();
|
||||
}
|
||||
|
||||
if (!window.storage.get('avatarsHaveBeenMigrated', false)) {
|
||||
window.ConversationController.migrateAvatarsForNonAcceptedConversations();
|
||||
}
|
||||
}
|
||||
|
||||
void badgeImageFileDownloader.checkForFilesToDownload();
|
||||
|
@@ -129,7 +129,7 @@ export function AddUserToAnotherGroupModal({
|
||||
}
|
||||
|
||||
return {
|
||||
...pick(convo, 'id', 'avatarUrl', 'title', 'unblurredAvatarUrl'),
|
||||
...pick(convo, 'id', 'avatarUrl', 'title', 'hasAvatar'),
|
||||
memberships,
|
||||
membersCount,
|
||||
disabledReason,
|
||||
|
@@ -4,7 +4,6 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { isBoolean } from 'lodash';
|
||||
import { expect, fn, within, userEvent } from '@storybook/test';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type { Props } from './Avatar';
|
||||
@@ -60,16 +59,13 @@ export default {
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest)
|
||||
? overrideProps.acceptedMessageRequest
|
||||
: true,
|
||||
avatarUrl: overrideProps.avatarUrl || '',
|
||||
badge: overrideProps.badge,
|
||||
blur: overrideProps.blur,
|
||||
color: overrideProps.color || AvatarColors[0],
|
||||
conversationType: overrideProps.conversationType || 'direct',
|
||||
hasAvatar: Boolean(overrideProps.hasAvatar),
|
||||
i18n,
|
||||
isMe: false,
|
||||
loading: Boolean(overrideProps.loading),
|
||||
noteToSelf: Boolean(overrideProps.noteToSelf),
|
||||
onClick: fn(action('onClick')),
|
||||
@@ -200,8 +196,9 @@ Loading.args = createProps({
|
||||
|
||||
export const BlurredBasedOnProps = TemplateSingle.bind({});
|
||||
BlurredBasedOnProps.args = createProps({
|
||||
acceptedMessageRequest: false,
|
||||
hasAvatar: true,
|
||||
avatarUrl: '/fixtures/kitten-3-64-64.jpg',
|
||||
blur: AvatarBlur.BlurPicture,
|
||||
});
|
||||
|
||||
export const ForceBlurred = TemplateSingle.bind({});
|
||||
|
@@ -25,8 +25,8 @@ import { assertDev } from '../util/assert';
|
||||
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
|
||||
import { getInitials } from '../util/getInitials';
|
||||
import { isBadgeVisible } from '../badges/isBadgeVisible';
|
||||
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
|
||||
import { SIGNAL_AVATAR_PATH } from '../types/SignalConversation';
|
||||
import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient';
|
||||
|
||||
export enum AvatarBlur {
|
||||
NoBlur,
|
||||
@@ -54,20 +54,18 @@ type BadgePlacementType = { bottom: number; right: number };
|
||||
|
||||
export type Props = {
|
||||
avatarUrl?: string;
|
||||
avatarPlaceholderGradient?: Readonly<[string, string]>;
|
||||
blur?: AvatarBlur;
|
||||
color?: AvatarColorType;
|
||||
hasAvatar?: boolean;
|
||||
loading?: boolean;
|
||||
|
||||
acceptedMessageRequest: boolean;
|
||||
conversationType: 'group' | 'direct' | 'callLink';
|
||||
isMe: boolean;
|
||||
noteToSelf?: boolean;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
sharedGroupNames: ReadonlyArray<string>;
|
||||
size: AvatarSize;
|
||||
title: string;
|
||||
unblurredAvatarUrl?: string;
|
||||
searchResult?: boolean;
|
||||
storyRing?: HasStories;
|
||||
|
||||
@@ -100,39 +98,26 @@ const BADGE_PLACEMENT_BY_SIZE = new Map<number, BadgePlacementType>([
|
||||
[112, { bottom: -4, right: 3 }],
|
||||
]);
|
||||
|
||||
const getDefaultBlur = (
|
||||
...args: Parameters<typeof shouldBlurAvatar>
|
||||
): AvatarBlur =>
|
||||
shouldBlurAvatar(...args) ? AvatarBlur.BlurPicture : AvatarBlur.NoBlur;
|
||||
|
||||
export function Avatar({
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
avatarPlaceholderGradient = getAvatarPlaceholderGradient(0),
|
||||
badge,
|
||||
className,
|
||||
color = 'A200',
|
||||
conversationType,
|
||||
hasAvatar,
|
||||
i18n,
|
||||
isMe,
|
||||
innerRef,
|
||||
loading,
|
||||
noteToSelf,
|
||||
onClick,
|
||||
onClickBadge,
|
||||
sharedGroupNames,
|
||||
size,
|
||||
theme,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
searchResult,
|
||||
storyRing,
|
||||
blur = getDefaultBlur({
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
isMe,
|
||||
sharedGroupNames,
|
||||
unblurredAvatarUrl,
|
||||
}),
|
||||
blur = AvatarBlur.NoBlur,
|
||||
...ariaProps
|
||||
}: Props): JSX.Element {
|
||||
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) {
|
||||
contentsChildren = (
|
||||
<div
|
||||
|
@@ -10,9 +10,11 @@ import { Lightbox } from './Lightbox';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
avatarPlaceholderGradient?: Readonly<[string, string]>;
|
||||
avatarColor?: AvatarColorType;
|
||||
avatarUrl?: string;
|
||||
conversationTitle?: string;
|
||||
hasAvatar?: boolean;
|
||||
i18n: LocalizerType;
|
||||
isGroup?: boolean;
|
||||
noteToSelf?: boolean;
|
||||
@@ -20,9 +22,11 @@ export type PropsType = {
|
||||
};
|
||||
|
||||
export function AvatarLightbox({
|
||||
avatarPlaceholderGradient,
|
||||
avatarColor,
|
||||
avatarUrl,
|
||||
conversationTitle,
|
||||
hasAvatar,
|
||||
i18n,
|
||||
isGroup,
|
||||
noteToSelf,
|
||||
@@ -44,9 +48,11 @@ export function AvatarLightbox({
|
||||
selectedIndex={0}
|
||||
>
|
||||
<AvatarPreview
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarColor={avatarColor}
|
||||
avatarUrl={avatarUrl}
|
||||
conversationTitle={conversationTitle}
|
||||
hasAvatar={hasAvatar}
|
||||
i18n={i18n}
|
||||
isGroup={isGroup}
|
||||
noteToSelf={noteToSelf}
|
||||
|
@@ -12,6 +12,7 @@ import type { AvatarColorType } from '../types/Colors';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import { getInitials } from '../util/getInitials';
|
||||
import { imagePathToBytes } from '../util/imagePathToBytes';
|
||||
import { type ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export type PropsType = {
|
||||
avatarColor?: AvatarColorType;
|
||||
@@ -26,19 +27,22 @@ export type PropsType = {
|
||||
onClear?: () => unknown;
|
||||
onClick?: () => unknown;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
} & Pick<ConversationType, 'avatarPlaceholderGradient' | 'hasAvatar'>;
|
||||
|
||||
enum ImageStatus {
|
||||
Nothing = 'nothing',
|
||||
Loading = 'loading',
|
||||
HasImage = 'has-image',
|
||||
HasPlaceholder = 'has-placeholder',
|
||||
}
|
||||
|
||||
export function AvatarPreview({
|
||||
avatarPlaceholderGradient,
|
||||
avatarColor = AvatarColors[0],
|
||||
avatarUrl,
|
||||
avatarValue,
|
||||
conversationTitle,
|
||||
hasAvatar,
|
||||
i18n,
|
||||
isEditable,
|
||||
isGroup,
|
||||
@@ -127,6 +131,8 @@ export function AvatarPreview({
|
||||
} else if (avatarUrl) {
|
||||
encodedPath = avatarUrl;
|
||||
imageStatus = ImageStatus.HasImage;
|
||||
} else if (hasAvatar && avatarPlaceholderGradient) {
|
||||
imageStatus = ImageStatus.HasPlaceholder;
|
||||
} else {
|
||||
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 (
|
||||
<div className="AvatarPreview">
|
||||
<div
|
||||
|
@@ -91,8 +91,6 @@ export function CallLinkAddNameModal({
|
||||
color={getColorForCallLink(callLink.rootKey)}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={
|
||||
callLink.name === ''
|
||||
|
@@ -107,8 +107,6 @@ export function CallLinkDetails({
|
||||
color={getColorForCallLink(callLink.rootKey)}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={callLink.name ?? i18n('icu:calling__call-link-default-title')}
|
||||
/>
|
||||
@@ -277,8 +275,6 @@ function renderMissingCallLink({
|
||||
badge={undefined}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={i18n('icu:calling__call-link-default-title')}
|
||||
/>
|
||||
|
@@ -128,8 +128,6 @@ export function CallLinkEditModal({
|
||||
color={getColorForCallLink(callLink.rootKey)}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={
|
||||
callLink.name === ''
|
||||
|
@@ -62,19 +62,18 @@ export function CallLinkPendingParticipantModal({
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
size={AvatarSize.EIGHTY}
|
||||
title={conversation.title}
|
||||
theme={ThemeType.dark}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
|
||||
/>
|
||||
|
||||
<button
|
||||
|
@@ -12,16 +12,17 @@ import type { ConversationType } from '../state/ducks/conversations';
|
||||
export type Props = {
|
||||
conversation: Pick<
|
||||
ConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'hasAvatar'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
i18n: LocalizerType;
|
||||
close: () => void;
|
||||
@@ -45,14 +46,14 @@ export function CallNeedPermissionScreen({
|
||||
return (
|
||||
<div className="module-call-need-permission-screen">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
badge={undefined}
|
||||
color={conversation.color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
phoneNumber={conversation.phoneNumber}
|
||||
profileName={conversation.profileName}
|
||||
title={conversation.title}
|
||||
|
@@ -500,14 +500,14 @@ export function CallScreen({
|
||||
) : (
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl}>
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPlaceholderGradient={me.avatarPlaceholderGradient}
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={undefined}
|
||||
color={me.color || AvatarColors[0]}
|
||||
hasAvatar={me.hasAvatar}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
title={me.title}
|
||||
|
@@ -79,7 +79,7 @@ function UnknownContacts({
|
||||
: 0;
|
||||
return (
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
className="CallingAdhocCallInfo__UnknownContactAvatar"
|
||||
@@ -87,7 +87,6 @@ function UnknownContacts({
|
||||
conversationType="direct"
|
||||
key={key}
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
@@ -210,13 +209,12 @@ export function CallingAdhocCallInfo({
|
||||
>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
|
@@ -35,9 +35,11 @@ export type PropsType = {
|
||||
callMode: CallMode;
|
||||
conversation: Pick<
|
||||
CallingConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'hasAvatar'
|
||||
| 'isMe'
|
||||
| 'memberships'
|
||||
| 'name'
|
||||
@@ -48,7 +50,6 @@ export type PropsType = {
|
||||
| 'systemNickname'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
getIsSharingPhoneNumberWithEverybody: () => boolean;
|
||||
groupMembers?: Array<
|
||||
|
@@ -126,15 +126,14 @@ export const CallingParticipantsList = React.memo(
|
||||
>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={
|
||||
participant.acceptedMessageRequest
|
||||
avatarPlaceholderGradient={
|
||||
participant.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
|
@@ -308,13 +308,14 @@ export function CallingPendingParticipants({
|
||||
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={
|
||||
participant.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
@@ -396,13 +397,12 @@ export function CallingPendingParticipants({
|
||||
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={participant.avatarPlaceholderGradient}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
|
@@ -40,11 +40,10 @@ function NoVideo({
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
color,
|
||||
type: conversationType,
|
||||
isMe,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
@@ -56,14 +55,13 @@ function NoVideo({
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
|
@@ -30,9 +30,11 @@ type PeekedParticipantType = Pick<
|
||||
export type PropsType = {
|
||||
conversation: Pick<
|
||||
CallingConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'hasAvatar'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
@@ -41,7 +43,6 @@ export type PropsType = {
|
||||
| 'systemNickname'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
i18n: LocalizerType;
|
||||
me: Pick<ConversationType, 'id' | 'serviceId'>;
|
||||
@@ -216,19 +217,18 @@ export function CallingPreCallInfo({
|
||||
return (
|
||||
<div className="module-CallingPreCallInfo">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
conversationType={conversation.type}
|
||||
isMe={conversation.isMe}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
noteToSelf={false}
|
||||
phoneNumber={conversation.phoneNumber}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
title={conversation.title}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<div className="module-CallingPreCallInfo__title">
|
||||
|
@@ -101,13 +101,15 @@ export function CallingRaisedHandsList({
|
||||
>
|
||||
<div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={
|
||||
participant.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={participant.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
|
@@ -900,12 +900,14 @@ export function CallsList({
|
||||
aria-selected={isSelected}
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPlaceholderGradient={
|
||||
conversation.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
color={conversation.color}
|
||||
conversationType={conversation.type}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={conversation.title}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
|
@@ -217,11 +217,12 @@ export function CallsNewCall({
|
||||
<ListTile
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPlaceholderGradient={
|
||||
item.conversation.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={item.conversation.avatarUrl}
|
||||
conversationType="group"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={item.conversation.title}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
|
@@ -14,32 +14,31 @@ export type PropsType = {
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'about'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'firstName'
|
||||
| 'hasAvatar'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
|
||||
export function ContactPill({
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
color,
|
||||
firstName,
|
||||
hasAvatar,
|
||||
i18n,
|
||||
isMe,
|
||||
id,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
onClickRemove,
|
||||
}: PropsType): JSX.Element {
|
||||
const removeLabel = i18n('icu:ContactPill--remove');
|
||||
@@ -47,20 +46,19 @@ export function ContactPill({
|
||||
return (
|
||||
<div className="module-ContactPill">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
hasAvatar={hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.TWENTY}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
/>
|
||||
<ContactName
|
||||
firstName={firstName}
|
||||
|
@@ -38,6 +38,7 @@ const contactPillProps = (
|
||||
getDefaultConversation({
|
||||
avatarUrl: gifUrl,
|
||||
firstName: 'John',
|
||||
hasAvatar: true,
|
||||
id: 'abc123',
|
||||
isMe: false,
|
||||
name: 'John Bon Bon Jovi',
|
||||
|
@@ -402,12 +402,14 @@ export function ConversationList({
|
||||
break;
|
||||
case RowType.Conversation: {
|
||||
const itemProps = pick(row.conversation, [
|
||||
'avatarPlaceholderGradient',
|
||||
'acceptedMessageRequest',
|
||||
'avatarUrl',
|
||||
'badges',
|
||||
'color',
|
||||
'draftPreview',
|
||||
'groupId',
|
||||
'hasAvatar',
|
||||
'id',
|
||||
'isBlocked',
|
||||
'isMe',
|
||||
@@ -425,7 +427,6 @@ export function ConversationList({
|
||||
'title',
|
||||
'type',
|
||||
'typingContactIdTimestamps',
|
||||
'unblurredAvatarUrl',
|
||||
'unreadCount',
|
||||
'unreadMentionsCount',
|
||||
'serviceId',
|
||||
|
@@ -51,19 +51,21 @@ export function DirectCallRemoteParticipant({
|
||||
function renderAvatar(
|
||||
i18n: LocalizerType,
|
||||
{
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
color,
|
||||
isMe,
|
||||
hasAvatar,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
}: Pick<
|
||||
ConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'hasAvatar'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
@@ -75,14 +77,14 @@ function renderAvatar(
|
||||
<div className="module-ongoing-call__remote-video-disabled">
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
hasAvatar={hasAvatar}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
|
@@ -90,16 +90,16 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
} = props;
|
||||
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
addedTime,
|
||||
avatarUrl,
|
||||
color,
|
||||
demuxId,
|
||||
hasAvatar,
|
||||
hasRemoteAudio,
|
||||
hasRemoteVideo,
|
||||
isHandRaised,
|
||||
isBlocked,
|
||||
isMe,
|
||||
mediaKeysReceived,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
@@ -455,14 +455,14 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
} else {
|
||||
noVideoNode = (
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
hasAvatar={hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
|
@@ -109,16 +109,15 @@ function Contacts({
|
||||
{contacts.map(contact => (
|
||||
<li key={contact.id} className="module-GroupDialog__contacts__contact">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
isMe={contact.isMe}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
noteToSelf={contact.isMe}
|
||||
theme={theme}
|
||||
title={contact.title}
|
||||
unblurredAvatarUrl={contact.unblurredAvatarUrl}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
i18n={i18n}
|
||||
|
@@ -69,14 +69,12 @@ export const GroupV2JoinDialog = React.memo(function GroupV2JoinDialogInner({
|
||||
/>
|
||||
<div className="module-group-v2-join-dialog__avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
avatarUrl={avatar ? avatar.url : undefined}
|
||||
badge={undefined}
|
||||
blur={AvatarBlur.NoBlur}
|
||||
loading={avatar && !avatar.url}
|
||||
conversationType="group"
|
||||
title={title}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={80}
|
||||
i18n={i18n}
|
||||
|
@@ -193,10 +193,8 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
|
||||
} = props;
|
||||
const {
|
||||
id: conversationId,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
color,
|
||||
isMe,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
@@ -274,14 +272,12 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
|
||||
<div className="IncomingCallBar__conversation">
|
||||
<div className="IncomingCallBar__conversation--avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
|
@@ -174,18 +174,19 @@ export function LeftPaneSearchInput({
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={searchConversation.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={
|
||||
searchConversation.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={searchConversation.avatarUrl}
|
||||
badge={undefined}
|
||||
color={searchConversation.color}
|
||||
conversationType={searchConversation.type}
|
||||
hasAvatar={searchConversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={searchConversation.isMe}
|
||||
noteToSelf={searchConversation.isMe}
|
||||
sharedGroupNames={searchConversation.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY}
|
||||
title={searchConversation.title}
|
||||
unblurredAvatarUrl={searchConversation.unblurredAvatarUrl}
|
||||
/>
|
||||
<button
|
||||
aria-label={i18n('icu:clearSearch')}
|
||||
|
@@ -882,19 +882,18 @@ function LightboxHeader({
|
||||
<div className="Lightbox__header--container">
|
||||
<div className="Lightbox__header--avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType={conversation.type}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
phoneNumber={conversation.e164}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={conversation.title}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="Lightbox__header--content">
|
||||
|
@@ -48,15 +48,7 @@ export function MyStoryButton({
|
||||
? getNewestMyStory(myStories[0])
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
color,
|
||||
isMe,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
} = me;
|
||||
const { avatarUrl, color, profileName, sharedGroupNames, title } = me;
|
||||
|
||||
if (!newestStory) {
|
||||
return (
|
||||
@@ -69,13 +61,11 @@ export function MyStoryButton({
|
||||
>
|
||||
<div className="MyStories__avatar-container">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={getAvatarColor(color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(isMe)}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
@@ -122,13 +112,11 @@ export function MyStoryButton({
|
||||
onContextMenuShowingChanged={setActive}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={getAvatarColor(color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(isMe)}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
|
@@ -372,14 +372,12 @@ export function NavTabs({
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span className="NavTabs__ItemContent">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
|
@@ -451,20 +451,19 @@ function ContactRow({
|
||||
return (
|
||||
<li className="module-SafetyNumberChangeDialog__row" key={contact.id}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
theme={theme}
|
||||
title={contact.title}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarUrl={contact.unblurredAvatarUrl}
|
||||
/>
|
||||
<div className="module-SafetyNumberChangeDialog__row--wrapper">
|
||||
<div className="module-SafetyNumberChangeDialog__row--name">
|
||||
|
@@ -557,13 +557,14 @@ export function SendStoryModal({
|
||||
htmlFor={id}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={
|
||||
group.avatarPlaceholderGradient
|
||||
}
|
||||
avatarUrl={group.avatarUrl}
|
||||
badge={undefined}
|
||||
color={group.color}
|
||||
conversationType={group.type}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={group.title}
|
||||
@@ -707,13 +708,11 @@ export function SendStoryModal({
|
||||
>
|
||||
{list.id === MY_STORY_ID ? (
|
||||
<Avatar
|
||||
acceptedMessageRequest={me.acceptedMessageRequest}
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={undefined}
|
||||
color={me.color}
|
||||
conversationType={me.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={me.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
storyRing={undefined}
|
||||
@@ -822,13 +821,11 @@ export function SendStoryModal({
|
||||
htmlFor={id}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||
avatarUrl={group.avatarUrl}
|
||||
badge={undefined}
|
||||
color={group.color}
|
||||
conversationType={group.type}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
storyRing={group.hasStories}
|
||||
|
@@ -160,13 +160,12 @@ function DistributionListItem({
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
{isMyStory ? (
|
||||
<Avatar
|
||||
acceptedMessageRequest={me.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={me.avatarPlaceholderGradient}
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={undefined}
|
||||
color={me.color}
|
||||
conversationType={me.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={me.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={me.title}
|
||||
@@ -214,13 +213,12 @@ function GroupStoryItem({
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={groupStory.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={groupStory.avatarPlaceholderGradient}
|
||||
avatarUrl={groupStory.avatarUrl}
|
||||
badge={undefined}
|
||||
color={groupStory.color}
|
||||
conversationType={groupStory.type}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={groupStory.title}
|
||||
@@ -675,13 +673,12 @@ export function DistributionListSettingsModal({
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={member.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={member.avatarPlaceholderGradient}
|
||||
avatarUrl={member.avatarUrl}
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
color={member.color}
|
||||
conversationType={member.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={member.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={theme}
|
||||
@@ -1094,13 +1091,12 @@ export function EditDistributionListModal({
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={theme}
|
||||
@@ -1190,10 +1186,10 @@ export function EditDistributionListModal({
|
||||
{selectedContacts.map(contact => (
|
||||
<ContactPill
|
||||
key={contact.id}
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
color={contact.color}
|
||||
firstName={contact.firstName}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
id={contact.id}
|
||||
isMe={contact.isMe}
|
||||
@@ -1287,13 +1283,12 @@ export function GroupStorySettingsModal({
|
||||
>
|
||||
<div className="GroupStorySettingsModal__header">
|
||||
<Avatar
|
||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={group.avatarPlaceholderGradient}
|
||||
avatarUrl={group.avatarUrl}
|
||||
badge={undefined}
|
||||
color={group.color}
|
||||
conversationType={group.type}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={group.title}
|
||||
@@ -1316,13 +1311,12 @@ export function GroupStorySettingsModal({
|
||||
className="GroupStorySettingsModal__members_item"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={member.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={member.avatarPlaceholderGradient}
|
||||
avatarUrl={member.avatarUrl}
|
||||
badge={undefined}
|
||||
color={member.color}
|
||||
conversationType={member.type}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
title={member.title}
|
||||
|
@@ -128,20 +128,17 @@ export function StoryDetailsModal({
|
||||
return (
|
||||
<div key={contact.id} className="StoryDetailsModal__contact">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={ThemeType.dark}
|
||||
title={contact.title}
|
||||
unblurredAvatarUrl={contact.unblurredAvatarUrl}
|
||||
/>
|
||||
<div className="StoryDetailsModal__contact__text">
|
||||
<ContactName title={contact.title} />
|
||||
@@ -171,13 +168,12 @@ export function StoryDetailsModal({
|
||||
</div>
|
||||
<div className="StoryDetailsModal__contact">
|
||||
<Avatar
|
||||
acceptedMessageRequest={sender.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={sender.avatarPlaceholderGradient}
|
||||
avatarUrl={sender.avatarUrl}
|
||||
badge={getPreferredBadge(sender.badges)}
|
||||
color={sender.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={sender.isMe}
|
||||
profileName={sender.profileName}
|
||||
sharedGroupNames={sender.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
|
@@ -34,21 +34,20 @@ export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
|
||||
};
|
||||
|
||||
function StoryListItemAvatar({
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
avatarStoryRing,
|
||||
badges,
|
||||
color,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
isMe,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
theme,
|
||||
}: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'profileName'
|
||||
@@ -59,18 +58,16 @@ function StoryListItemAvatar({
|
||||
badges?: ConversationType['badges'];
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
isMe?: boolean;
|
||||
theme: ThemeType;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={badges ? getPreferredBadge(badges) : undefined}
|
||||
color={getAvatarColor(color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(isMe)}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
|
@@ -201,7 +201,6 @@ export function StoryViewer({
|
||||
timestamp,
|
||||
} = story;
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
color,
|
||||
isMe,
|
||||
@@ -737,13 +736,11 @@ export function StoryViewer({
|
||||
<div className="StoryViewer__meta__playback-bar">
|
||||
<div className="StoryViewer__meta__playback-bar__container">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={getAvatarColor(color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(isMe)}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
@@ -751,14 +748,12 @@ export function StoryViewer({
|
||||
/>
|
||||
{group && (
|
||||
<Avatar
|
||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||
avatarUrl={group.avatarUrl}
|
||||
badge={undefined}
|
||||
className="StoryViewer__meta--group-avatar"
|
||||
color={getAvatarColor(group.color)}
|
||||
conversationType="group"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
profileName={group.profileName}
|
||||
sharedGroupNames={group.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
|
@@ -382,13 +382,11 @@ export function StoryViewsNRepliesModal({
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
acceptedMessageRequest={view.recipient.acceptedMessageRequest}
|
||||
avatarUrl={view.recipient.avatarUrl}
|
||||
badge={undefined}
|
||||
color={getAvatarColor(view.recipient.color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(view.recipient.isMe)}
|
||||
profileName={view.recipient.profileName}
|
||||
sharedGroupNames={view.recipient.sharedGroupNames || []}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
@@ -564,13 +562,11 @@ function ReplyOrReactionMessage({
|
||||
>
|
||||
<div className="StoryViewsNRepliesModal__reaction--container">
|
||||
<Avatar
|
||||
acceptedMessageRequest={reply.author.acceptedMessageRequest}
|
||||
avatarUrl={reply.author.avatarUrl}
|
||||
badge={getPreferredBadge(reply.author.badges)}
|
||||
color={getAvatarColor(reply.author.color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(reply.author.isMe)}
|
||||
profileName={reply.author.profileName}
|
||||
sharedGroupNames={reply.author.sharedGroupNames || []}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
|
@@ -68,7 +68,8 @@ export default {
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
pendingAvatarDownload: false,
|
||||
conversation,
|
||||
fromOrAddedByTrustedContact: false,
|
||||
isSignalConnection: false,
|
||||
|
@@ -1,11 +1,10 @@
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// 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 { LocalizerType } from '../../types/Util';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { Avatar, AvatarBlur, AvatarSize } from '../Avatar';
|
||||
import { Modal } from '../Modal';
|
||||
import { UserText } from '../UserText';
|
||||
@@ -28,11 +27,12 @@ export type PropsType = Readonly<{
|
||||
conversation: ConversationType;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
isSignalConnection: boolean;
|
||||
pendingAvatarDownload?: boolean;
|
||||
startAvatarDownload?: (id: string) => unknown;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
toggleSafetyNumberModal: (id: string) => void;
|
||||
toggleProfileNameWarningModal: () => void;
|
||||
updateSharedGroups: (id: string) => void;
|
||||
unblurAvatar: (conversationId: string) => void;
|
||||
}>;
|
||||
|
||||
export function AboutContactModal({
|
||||
@@ -40,30 +40,44 @@ export function AboutContactModal({
|
||||
conversation,
|
||||
fromOrAddedByTrustedContact,
|
||||
isSignalConnection,
|
||||
pendingAvatarDownload,
|
||||
startAvatarDownload,
|
||||
toggleSignalConnectionsModal,
|
||||
toggleSafetyNumberModal,
|
||||
toggleProfileNameWarningModal,
|
||||
updateSharedGroups,
|
||||
unblurAvatar,
|
||||
onClose,
|
||||
onOpenNotePreviewModal,
|
||||
}: PropsType): JSX.Element {
|
||||
const { isMe } = conversation;
|
||||
const { avatarUrl, hasAvatar, isMe } = conversation;
|
||||
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups(conversation.id);
|
||||
}, [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.NoBlur;
|
||||
|
||||
const onAvatarClick = useCallback(() => {
|
||||
if (avatarBlur === AvatarBlur.BlurPictureWithClickToView) {
|
||||
unblurAvatar(conversation.id);
|
||||
const avatarOnClick = useMemo(() => {
|
||||
if (!enableClickToLoad) {
|
||||
return undefined;
|
||||
}
|
||||
}, [avatarBlur, unblurAvatar, conversation.id]);
|
||||
return () => {
|
||||
if (!pendingAvatarDownload && startAvatarDownload) {
|
||||
startAvatarDownload(conversation.id);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
conversation.id,
|
||||
startAvatarDownload,
|
||||
enableClickToLoad,
|
||||
pendingAvatarDownload,
|
||||
]);
|
||||
|
||||
const onSignalConnectionClick = useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
@@ -131,20 +145,20 @@ export function AboutContactModal({
|
||||
>
|
||||
<div className="AboutContactModal__row AboutContactModal__row--centered">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
blur={avatarBlur}
|
||||
onClick={avatarBlur === AvatarBlur.NoBlur ? undefined : onAvatarClick}
|
||||
onClick={avatarOnClick}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
loading={pendingAvatarDownload && !conversation.avatarUrl}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWO_HUNDRED_SIXTEEN}
|
||||
title={conversation.title}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@@ -53,6 +53,7 @@ export default {
|
||||
),
|
||||
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||
showConversation: action('showConversation'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
theme: ThemeType.light,
|
||||
toggleAboutContactModal: action('AboutContactModal'),
|
||||
toggleAdmin: action('toggleAdmin'),
|
||||
|
@@ -14,7 +14,7 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import * as log from '../../logging/log';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { Avatar, AvatarBlur, AvatarSize } from '../Avatar';
|
||||
import { AvatarLightbox } from '../AvatarLightbox';
|
||||
import { BadgeDialog } from '../BadgeDialog';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
@@ -55,6 +55,7 @@ type PropsActionType = {
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
|
||||
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
startAvatarDownload: () => void;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
togglePip: () => void;
|
||||
@@ -98,6 +99,7 @@ export function ContactModal({
|
||||
onOutgoingVideoCallInConversation,
|
||||
removeMemberFromGroup,
|
||||
showConversation,
|
||||
startAvatarDownload,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
@@ -310,13 +312,18 @@ export function ContactModal({
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={preferredBadge}
|
||||
blur={
|
||||
!contact.avatarUrl && !contact.isMe && contact.hasAvatar
|
||||
? AvatarBlur.BlurPictureWithClickToView
|
||||
: AvatarBlur.NoBlur
|
||||
}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
onClick={() => {
|
||||
if (conversation && hasStories) {
|
||||
viewUserStories({
|
||||
@@ -324,6 +331,12 @@ export function ContactModal({
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
});
|
||||
hideContactModal();
|
||||
} else if (
|
||||
!contact.avatarUrl &&
|
||||
!contact.isMe &&
|
||||
contact.hasAvatar
|
||||
) {
|
||||
startAvatarDownload();
|
||||
} else {
|
||||
setView(ContactModalView.ShowingAvatar);
|
||||
}
|
||||
@@ -335,7 +348,6 @@ export function ContactModal({
|
||||
storyRing={hasStories}
|
||||
theme={theme}
|
||||
title={contact.title}
|
||||
unblurredAvatarUrl={contact.unblurredAvatarUrl}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -468,9 +480,11 @@ export function ContactModal({
|
||||
case ContactModalView.ShowingAvatar:
|
||||
return (
|
||||
<AvatarLightbox
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarColor={contact.color}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
conversationTitle={contact.title}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
onClose={() => setView(ContactModalView.Default)}
|
||||
/>
|
||||
|
@@ -460,13 +460,17 @@ function HeaderContent({
|
||||
const avatar = (
|
||||
<span className="module-ConversationHeader__header__avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={
|
||||
conversation.gradientStart && conversation.gradientEnd
|
||||
? [conversation.gradientStart, conversation.gradientEnd]
|
||||
: undefined
|
||||
}
|
||||
avatarUrl={conversation.avatarUrl ?? undefined}
|
||||
badge={badge ?? undefined}
|
||||
color={conversation.color ?? undefined}
|
||||
conversationType={conversation.type}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
noteToSelf={conversation.isMe}
|
||||
onClick={hasStories ? onViewUserStories : onClick}
|
||||
phoneNumber={conversation.phoneNumber ?? undefined}
|
||||
@@ -477,7 +481,6 @@ function HeaderContent({
|
||||
storyRing={conversation.isMe ? undefined : (hasStories ?? undefined)}
|
||||
theme={theme}
|
||||
title={conversation.title}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl ?? undefined}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
@@ -23,12 +23,13 @@ export default {
|
||||
i18n,
|
||||
isDirectConvoAndHasNickname: false,
|
||||
theme: ThemeType.light,
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
viewUserStories: action('viewUserStories'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
openConversationDetails: action('openConversationDetails'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
pendingAvatarDownload: false,
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
|
@@ -13,7 +13,6 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { HasStories } from '../../types/Stories';
|
||||
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||
import { I18n } from '../I18n';
|
||||
@@ -23,6 +22,7 @@ export type Props = {
|
||||
acceptedMessageRequest?: boolean;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
groupDescription?: string;
|
||||
hasAvatar?: boolean;
|
||||
hasStories?: HasStories;
|
||||
id: string;
|
||||
i18n: LocalizerType;
|
||||
@@ -31,10 +31,10 @@ export type Props = {
|
||||
isSignalConversation?: boolean;
|
||||
membersCount?: number;
|
||||
openConversationDetails?: () => unknown;
|
||||
pendingAvatarDownload?: boolean;
|
||||
phoneNumber?: string;
|
||||
sharedGroupNames?: ReadonlyArray<string>;
|
||||
unblurAvatar: (conversationId: string) => void;
|
||||
unblurredAvatarUrl?: string;
|
||||
startAvatarDownload: () => void;
|
||||
updateSharedGroups: (conversationId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
@@ -57,6 +57,7 @@ const renderExtraInformation = ({
|
||||
sharedGroupNames,
|
||||
}: Pick<
|
||||
Props,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'conversationType'
|
||||
| 'fromOrAddedByTrustedContact'
|
||||
@@ -227,6 +228,7 @@ function ReleaseNotesExtraInformation({
|
||||
}
|
||||
|
||||
export function ConversationHero({
|
||||
avatarPlaceholderGradient,
|
||||
i18n,
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
@@ -236,6 +238,7 @@ export function ConversationHero({
|
||||
conversationType,
|
||||
fromOrAddedByTrustedContact,
|
||||
groupDescription,
|
||||
hasAvatar,
|
||||
hasStories,
|
||||
id,
|
||||
isDirectConvoAndHasNickname,
|
||||
@@ -243,13 +246,13 @@ export function ConversationHero({
|
||||
openConversationDetails,
|
||||
isSignalConversation,
|
||||
membersCount,
|
||||
pendingAvatarDownload,
|
||||
sharedGroupNames = [],
|
||||
phoneNumber,
|
||||
profileName,
|
||||
startAvatarDownload,
|
||||
theme,
|
||||
title,
|
||||
unblurAvatar,
|
||||
unblurredAvatarUrl,
|
||||
updateSharedGroups,
|
||||
viewUserStories,
|
||||
toggleAboutContactModal,
|
||||
@@ -264,17 +267,14 @@ export function ConversationHero({
|
||||
|
||||
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
|
||||
let avatarOnClick: undefined | (() => void);
|
||||
if (
|
||||
shouldBlurAvatar({
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
isMe,
|
||||
sharedGroupNames,
|
||||
unblurredAvatarUrl,
|
||||
})
|
||||
) {
|
||||
|
||||
if (!avatarUrl && !isMe && hasAvatar) {
|
||||
avatarBlur = AvatarBlur.BlurPictureWithClickToView;
|
||||
avatarOnClick = () => unblurAvatar(id);
|
||||
avatarOnClick = () => {
|
||||
if (!pendingAvatarDownload) {
|
||||
startAvatarDownload();
|
||||
}
|
||||
};
|
||||
} else if (hasStories) {
|
||||
avatarOnClick = () => {
|
||||
viewUserStories({
|
||||
@@ -312,7 +312,7 @@ export function ConversationHero({
|
||||
<>
|
||||
<div className="module-conversation-hero">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={badge}
|
||||
blur={avatarBlur}
|
||||
@@ -320,7 +320,8 @@ export function ConversationHero({
|
||||
color={color}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
hasAvatar={hasAvatar}
|
||||
loading={pendingAvatarDownload && !avatarUrl}
|
||||
noteToSelf={isMe}
|
||||
onClick={avatarOnClick}
|
||||
profileName={profileName}
|
||||
|
@@ -279,18 +279,19 @@ export type PropsData = {
|
||||
contact?: ReadonlyDeep<EmbeddedContactForUIType>;
|
||||
author: Pick<
|
||||
ConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'badges'
|
||||
| 'color'
|
||||
| 'firstName'
|
||||
| 'hasAvatar'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
conversationType: ConversationTypeType;
|
||||
attachments?: ReadonlyArray<AttachmentForUIType>;
|
||||
@@ -1578,12 +1579,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
{first.isCallLink && (
|
||||
<div className="module-message__link-preview__call-link-icon">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
badge={undefined}
|
||||
color={getColorForCallLink(getKeyFromCallLink(first.url))}
|
||||
conversationType="callLink"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={64}
|
||||
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} />
|
||||
) : (
|
||||
<Avatar
|
||||
acceptedMessageRequest={author.acceptedMessageRequest}
|
||||
avatarUrl={author.avatarUrl}
|
||||
badge={getPreferredBadge(author.badges)}
|
||||
color={author.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={author.isMe}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -2192,7 +2189,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
size={GROUP_AVATAR_SIZE}
|
||||
theme={theme}
|
||||
title={author.title}
|
||||
unblurredAvatarUrl={author.unblurredAvatarUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -49,7 +49,6 @@ export type Contact = Pick<
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarUrl'
|
||||
> & {
|
||||
status?: SendStatus;
|
||||
statusTimestamp?: number;
|
||||
@@ -168,34 +167,28 @@ export function MessageDetail({
|
||||
|
||||
function renderAvatar(contact: Contact): JSX.Element {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badges,
|
||||
color,
|
||||
isMe,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
} = contact;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={getPreferredBadge(badges)}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
theme={theme}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -257,13 +257,11 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||
>
|
||||
<div className="module-reaction-viewer__body__row__avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={from.acceptedMessageRequest}
|
||||
avatarUrl={from.avatarUrl}
|
||||
badge={getPreferredBadge(from.badges)}
|
||||
conversationType="direct"
|
||||
sharedGroupNames={from.sharedGroupNames}
|
||||
size={32}
|
||||
isMe={from.isMe}
|
||||
color={from.color}
|
||||
profileName={from.profileName}
|
||||
phoneNumber={from.phoneNumber}
|
||||
|
@@ -124,12 +124,10 @@ export function Thumbnail({
|
||||
return (
|
||||
<div className={getClassName('__icon-container-call-link')}>
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
badge={undefined}
|
||||
color={getColorForCallLink(getKeyFromCallLink(url))}
|
||||
conversationType="callLink"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
size={64}
|
||||
title={title ?? i18n('icu:calling__call-link-default-title')}
|
||||
|
@@ -334,8 +334,6 @@ const actions = () => ({
|
||||
closeContactSpoofingReview: action('closeContactSpoofingReview'),
|
||||
reviewConversationNameCollision: action('reviewConversationNameCollision'),
|
||||
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
|
||||
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
|
||||
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
|
||||
|
||||
@@ -346,6 +344,8 @@ const actions = () => ({
|
||||
onOpenMessageRequestActionsConfirmation: action(
|
||||
'onOpenMessageRequestActionsConfirmation'
|
||||
),
|
||||
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
});
|
||||
|
||||
const renderItem = ({
|
||||
@@ -416,7 +416,8 @@ const renderHeroRow = () => {
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
theme={theme}
|
||||
title={getTitle()}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
startAvatarDownload={action('startAvatarDownload')}
|
||||
pendingAvatarDownload={false}
|
||||
updateSharedGroups={noop}
|
||||
viewUserStories={action('viewUserStories')}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
|
@@ -123,13 +123,11 @@ function TypingBubbleAvatar({
|
||||
return (
|
||||
<animated.div className="module-message__typing-avatar" style={springProps}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
@@ -34,14 +34,12 @@ export function renderAvatar({
|
||||
|
||||
const renderAttachmentDownloaded = () => (
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
blur={AvatarBlur.NoBlur}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
title={title}
|
||||
sharedGroupNames={[]}
|
||||
size={size}
|
||||
|
@@ -411,10 +411,11 @@ export function ChooseGroupMembersModal({
|
||||
{selectedContacts.map(contact => (
|
||||
<ContactPill
|
||||
key={contact.id}
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
color={contact.color}
|
||||
firstName={contact.systemGivenName ?? contact.firstName}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
id={contact.id}
|
||||
|
@@ -87,6 +87,7 @@ const createProps = (
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showConversation: action('showConversation'),
|
||||
showLightbox: action('showLightbox'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
|
@@ -94,8 +94,10 @@ export type StateProps = {
|
||||
maxRecommendedGroupSize: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingAvatarDownload?: boolean;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
selectedNavTab: NavTab;
|
||||
startAvatarDownload: () => void;
|
||||
theme: ThemeType;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
renderChooseGroupMembersModal: (
|
||||
@@ -197,6 +199,7 @@ export function ConversationDetails({
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
pendingApprovalMemberships,
|
||||
pendingAvatarDownload,
|
||||
pendingMemberships,
|
||||
pushPanelForConversation,
|
||||
renderChooseGroupMembersModal,
|
||||
@@ -210,6 +213,7 @@ export function ConversationDetails({
|
||||
showContactModal,
|
||||
showConversation,
|
||||
showLightbox,
|
||||
startAvatarDownload,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleSafetyNumberModal,
|
||||
@@ -410,6 +414,8 @@ export function ConversationDetails({
|
||||
isGroup={isGroup}
|
||||
isSignalConversation={isSignalConversation}
|
||||
membersCount={conversation.membersCount ?? null}
|
||||
pendingAvatarDownload={pendingAvatarDownload ?? false}
|
||||
startAvatarDownload={startAvatarDownload}
|
||||
startEditing={(isGroupTitle: boolean) => {
|
||||
setModalState(
|
||||
isGroupTitle
|
||||
|
@@ -43,6 +43,8 @@ function Wrapper(overrideProps: Partial<Props>) {
|
||||
isGroup
|
||||
isMe={false}
|
||||
isSignalConversation={false}
|
||||
pendingAvatarDownload={false}
|
||||
startAvatarDownload={action('startAvatarDownload')}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
{...overrideProps}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Avatar, AvatarSize } from '../../Avatar';
|
||||
import { Avatar, AvatarBlur, AvatarSize } from '../../Avatar';
|
||||
import { AvatarLightbox } from '../../AvatarLightbox';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { GroupDescription } from '../GroupDescription';
|
||||
@@ -27,6 +27,8 @@ export type Props = {
|
||||
isMe: boolean;
|
||||
isSignalConversation: boolean;
|
||||
membersCount: number | null;
|
||||
pendingAvatarDownload: boolean;
|
||||
startAvatarDownload: () => void;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
theme: ThemeType;
|
||||
@@ -47,6 +49,8 @@ export function ConversationDetailsHeader({
|
||||
isMe,
|
||||
isSignalConversation,
|
||||
membersCount,
|
||||
pendingAvatarDownload,
|
||||
startAvatarDownload,
|
||||
startEditing,
|
||||
toggleAboutContactModal,
|
||||
theme,
|
||||
@@ -84,8 +88,15 @@ export function ConversationDetailsHeader({
|
||||
preferredBadge = badges?.[0];
|
||||
}
|
||||
|
||||
const shouldShowClickToView =
|
||||
!conversation.avatarUrl && !isMe && conversation.hasAvatar;
|
||||
const avatarBlur = shouldShowClickToView
|
||||
? AvatarBlur.BlurPictureWithClickToView
|
||||
: AvatarBlur.NoBlur;
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
blur={avatarBlur}
|
||||
badge={preferredBadge}
|
||||
conversationType={conversation.type}
|
||||
i18n={i18n}
|
||||
@@ -93,9 +104,18 @@ export function ConversationDetailsHeader({
|
||||
{...conversation}
|
||||
noteToSelf={isMe}
|
||||
onClick={() => {
|
||||
if (shouldShowClickToView) {
|
||||
startAvatarDownload();
|
||||
return;
|
||||
}
|
||||
setActiveModal(ConversationDetailsHeaderActiveModal.ShowingAvatar);
|
||||
}}
|
||||
loading={pendingAvatarDownload}
|
||||
onClickBadge={() => {
|
||||
if (shouldShowClickToView) {
|
||||
startAvatarDownload();
|
||||
return;
|
||||
}
|
||||
setActiveModal(ConversationDetailsHeaderActiveModal.ShowingBadges);
|
||||
}}
|
||||
sharedGroupNames={[]}
|
||||
@@ -108,9 +128,11 @@ export function ConversationDetailsHeader({
|
||||
case ConversationDetailsHeaderActiveModal.ShowingAvatar:
|
||||
modal = (
|
||||
<AvatarLightbox
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarColor={conversation.color}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
conversationTitle={conversation.title}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
isGroup={isGroup}
|
||||
noteToSelf={isMe}
|
||||
|
@@ -58,17 +58,17 @@ type PropsType = {
|
||||
testId?: string;
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'avatarUrl'
|
||||
| 'color'
|
||||
| 'groupId'
|
||||
| 'hasAvatar'
|
||||
| 'isMe'
|
||||
| 'markedUnread'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'unblurredAvatarUrl'
|
||||
| 'serviceId'
|
||||
> &
|
||||
(
|
||||
@@ -79,7 +79,7 @@ type PropsType = {
|
||||
export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||
React.memo(function BaseConversationListItem(props) {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
avatarSize,
|
||||
buttonAriaLabel,
|
||||
@@ -88,6 +88,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||
conversationType,
|
||||
disabled,
|
||||
groupId,
|
||||
hasAvatar,
|
||||
headerDate,
|
||||
headerName,
|
||||
i18n,
|
||||
@@ -108,7 +109,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||
shouldShowSpinner,
|
||||
testId: overrideTestId,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
serviceId,
|
||||
@@ -195,20 +195,19 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||
<>
|
||||
<div className={AVATAR_CONTAINER_CLASS_NAME}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
color={color}
|
||||
conversationType={conversationType}
|
||||
hasAvatar={hasAvatar}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
searchResult={isUsernameSearchResult}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={avatarSize ?? AvatarSize.FORTY_EIGHT}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
// This is here to appease the type checker.
|
||||
{...(props.badge
|
||||
? { badge: props.badge, theme: props.theme }
|
||||
|
@@ -37,7 +37,6 @@ export type PropsDataType = {
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
| 'serviceId'
|
||||
>;
|
||||
|
||||
@@ -55,7 +54,6 @@ type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
function ContactCheckbox({
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badge,
|
||||
color,
|
||||
@@ -71,7 +69,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
theme,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
}) {
|
||||
const disabled = Boolean(disabledReason);
|
||||
|
||||
@@ -103,19 +100,16 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
isChecked={isChecked}
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
noteToSelf={Boolean(isMe)}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
// appease the type checker.
|
||||
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||
/>
|
||||
|
@@ -37,7 +37,6 @@ export type ContactListItemConversationType = Pick<
|
||||
| 'systemFamilyName'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
| 'username'
|
||||
| 'e164'
|
||||
| 'serviceId'
|
||||
@@ -63,7 +62,6 @@ type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||
function ContactListItem({
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badge,
|
||||
color,
|
||||
@@ -85,7 +83,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||
theme,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
serviceId,
|
||||
}) {
|
||||
const [isConfirmingBlocking, setConfirmingBlocking] = useState(false);
|
||||
@@ -264,19 +261,16 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||
moduleClassName="ContactListItem"
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
noteToSelf={Boolean(isMe)}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
// This is here to appease the type checker.
|
||||
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||
/>
|
||||
|
@@ -38,12 +38,14 @@ export type MessageStatusType = (typeof MessageStatuses)[number];
|
||||
|
||||
export type PropsData = Pick<
|
||||
ConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'badges'
|
||||
| 'color'
|
||||
| 'draftPreview'
|
||||
| 'groupId'
|
||||
| 'hasAvatar'
|
||||
| 'id'
|
||||
| 'isBlocked'
|
||||
| 'isMe'
|
||||
@@ -62,7 +64,6 @@ export type PropsData = Pick<
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'typingContactIdTimestamps'
|
||||
| 'unblurredAvatarUrl'
|
||||
| 'unreadCount'
|
||||
| 'unreadMentionsCount'
|
||||
| 'serviceId'
|
||||
@@ -82,6 +83,7 @@ export type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||
function ConversationListItem({
|
||||
avatarPlaceholderGradient,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badge,
|
||||
@@ -89,6 +91,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||
color,
|
||||
draftPreview,
|
||||
groupId,
|
||||
hasAvatar,
|
||||
i18n,
|
||||
id,
|
||||
isBlocked,
|
||||
@@ -109,7 +112,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||
title,
|
||||
type,
|
||||
typingContactIdTimestamps,
|
||||
unblurredAvatarUrl,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
serviceId,
|
||||
@@ -211,13 +213,14 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={badge}
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
groupId={groupId}
|
||||
hasAvatar={hasAvatar}
|
||||
headerDate={lastUpdated}
|
||||
headerName={headerName}
|
||||
i18n={i18n}
|
||||
@@ -237,7 +240,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||
title={title}
|
||||
unreadCount={unreadCount}
|
||||
unreadMentionsCount={unreadMentionsCount}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
);
|
||||
|
@@ -16,7 +16,7 @@ export enum DisabledReason {
|
||||
|
||||
export type GroupListItemConversationType = Pick<
|
||||
ConversationType,
|
||||
'id' | 'title' | 'avatarUrl'
|
||||
'avatarPlaceholderGradient' | 'id' | 'title' | 'avatarUrl' | 'hasAvatar'
|
||||
> & {
|
||||
disabledReason: DisabledReason | undefined;
|
||||
membersCount: number;
|
||||
@@ -55,11 +55,11 @@ export function GroupListItem({
|
||||
<ListTile
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPlaceholderGradient={group.avatarPlaceholderGradient}
|
||||
avatarUrl={group.avatarUrl}
|
||||
conversationType="group"
|
||||
hasAvatar={group.hasAvatar}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={group.title}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
|
@@ -48,7 +48,6 @@ export type PropsDataType = {
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
|
||||
to: Pick<
|
||||
@@ -183,7 +182,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={from.acceptedMessageRequest}
|
||||
avatarUrl={from.avatarUrl}
|
||||
badge={getPreferredBadge(from.badges)}
|
||||
color={from.color}
|
||||
@@ -192,8 +190,8 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||
headerName={headerName}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isNoteToSelf={isNoteToSelf}
|
||||
isMe={from.isMe}
|
||||
isNoteToSelf={isNoteToSelf}
|
||||
isSelected={false}
|
||||
messageText={messageText}
|
||||
onClick={onClickItem}
|
||||
@@ -202,7 +200,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||
sharedGroupNames={from.sharedGroupNames}
|
||||
theme={theme}
|
||||
title={from.title}
|
||||
unblurredAvatarUrl={from.unblurredAvatarUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -93,11 +93,9 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
phoneNumber={phoneNumber.userInput}
|
||||
title={phoneNumber.userInput}
|
||||
sharedGroupNames={[]}
|
||||
|
@@ -92,11 +92,9 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
||||
<ListTile
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
conversationType="direct"
|
||||
searchResult
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={phoneNumber.userInput}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
badge={undefined}
|
||||
|
@@ -66,12 +66,10 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
searchResult
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={title}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
|
@@ -61,11 +61,9 @@ export function UsernameSearchResultListItem({
|
||||
<ListTile
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={false}
|
||||
conversationType="direct"
|
||||
searchResult
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={username}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
badge={undefined}
|
||||
|
@@ -214,11 +214,12 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||
<ContactPills>
|
||||
{this.#selectedContacts.map(contact => (
|
||||
<ContactPill
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
key={contact.id}
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
color={contact.color}
|
||||
firstName={contact.systemGivenName ?? contact.firstName}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
id={contact.id}
|
||||
isMe={contact.isMe}
|
||||
|
76
ts/groups.ts
76
ts/groups.ts
@@ -104,6 +104,8 @@ import { getProfile } from './util/getProfile';
|
||||
import { generateMessageId } from './util/generateMessageId';
|
||||
import { postSaveUpdates } from './util/cleanup';
|
||||
import { MessageModel } from './models/messages';
|
||||
import { areWePending } from './util/groupMembershipUtils';
|
||||
import { isConversationAccepted } from './util/isConversationAccepted';
|
||||
|
||||
type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
@@ -3792,11 +3794,11 @@ async function updateGroupViaPreJoinInfo({
|
||||
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
...(await applyNewAvatar(
|
||||
dropNull(preJoinInfo.avatar),
|
||||
newAttributes,
|
||||
logId
|
||||
)),
|
||||
...(await applyNewAvatar({
|
||||
newAvatarUrl: dropNull(preJoinInfo.avatar),
|
||||
attributes: newAttributes,
|
||||
logId,
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -5441,7 +5443,11 @@ async function applyGroupChange({
|
||||
const { avatar } = actions.modifyAvatar;
|
||||
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
|
||||
export async function applyNewAvatar(
|
||||
newAvatarUrl: string | undefined,
|
||||
attributes: Readonly<
|
||||
Pick<ConversationAttributesType, 'avatar' | 'secretParams'>
|
||||
>,
|
||||
logId: string
|
||||
): Promise<Pick<ConversationAttributesType, 'avatar'>> {
|
||||
const result: Pick<ConversationAttributesType, 'avatar'> = {};
|
||||
options:
|
||||
| {
|
||||
newAvatarUrl?: string | undefined;
|
||||
attributes: Pick<ConversationAttributesType, 'avatar' | 'secretParams'>;
|
||||
logId: string;
|
||||
forceDownload: true;
|
||||
}
|
||||
| {
|
||||
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 {
|
||||
// Avatar has been dropped
|
||||
if (!newAvatarUrl && attributes.avatar) {
|
||||
@@ -5739,19 +5755,34 @@ export async function applyNewAvatar(
|
||||
result.avatar = undefined;
|
||||
}
|
||||
|
||||
const avatarUrlToUse =
|
||||
newAvatarUrl ||
|
||||
('remoteAvatarUrl' in attributes
|
||||
? attributes.remoteAvatarUrl
|
||||
: undefined);
|
||||
|
||||
// Group has avatar; has it changed?
|
||||
if (
|
||||
newAvatarUrl &&
|
||||
(!attributes.avatar?.path || attributes.avatar.url !== newAvatarUrl)
|
||||
avatarUrlToUse &&
|
||||
(!attributes.avatar?.path || attributes.avatar.url !== avatarUrlToUse)
|
||||
) {
|
||||
if (!attributes.secretParams) {
|
||||
throw new Error('applyNewAvatar: group was missing secretParams!');
|
||||
}
|
||||
|
||||
const data = await decryptGroupAvatar(
|
||||
newAvatarUrl,
|
||||
if (
|
||||
!forceDownload &&
|
||||
(areWePending(attributes) || !isConversationAccepted(attributes))
|
||||
) {
|
||||
result.remoteAvatarUrl = avatarUrlToUse;
|
||||
return result;
|
||||
}
|
||||
|
||||
const data: Uint8Array = await decryptGroupAvatar(
|
||||
avatarUrlToUse,
|
||||
attributes.secretParams
|
||||
);
|
||||
|
||||
const hash = computeHash(data);
|
||||
|
||||
if (attributes.avatar?.hash === hash) {
|
||||
@@ -5760,7 +5791,7 @@ export async function applyNewAvatar(
|
||||
);
|
||||
result.avatar = {
|
||||
...attributes.avatar,
|
||||
url: newAvatarUrl,
|
||||
url: avatarUrlToUse,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
@@ -5770,10 +5801,9 @@ export async function applyNewAvatar(
|
||||
attributes.avatar.path
|
||||
);
|
||||
}
|
||||
|
||||
const local = await window.Signal.Migrations.writeNewAttachmentData(data);
|
||||
result.avatar = {
|
||||
url: newAvatarUrl,
|
||||
url: avatarUrlToUse,
|
||||
...local,
|
||||
hash,
|
||||
};
|
||||
@@ -5871,7 +5901,11 @@ async function applyGroupState({
|
||||
// avatar
|
||||
result = {
|
||||
...result,
|
||||
...(await applyNewAvatar(dropNull(groupState.avatar), result, logId)),
|
||||
...(await applyNewAvatar({
|
||||
newAvatarUrl: dropNull(groupState.avatar),
|
||||
attributes: result,
|
||||
logId,
|
||||
})),
|
||||
};
|
||||
|
||||
// disappearingMessagesTimer
|
||||
|
@@ -415,7 +415,12 @@ export async function joinViaLink(value: string): Promise<void> {
|
||||
secretParams,
|
||||
};
|
||||
try {
|
||||
const patch = await applyNewAvatar(result.avatar, attributes, logId);
|
||||
const patch = await applyNewAvatar({
|
||||
newAvatarUrl: result.avatar,
|
||||
attributes,
|
||||
logId,
|
||||
forceDownload: true,
|
||||
});
|
||||
attributes = { ...attributes, ...patch };
|
||||
|
||||
if (attributes.avatar && attributes.avatar.path) {
|
||||
|
@@ -28,6 +28,7 @@ export type MinimalConversation = Satisfies<
|
||||
| 'color'
|
||||
| 'expireTimer'
|
||||
| 'groupVersion'
|
||||
| 'hasAvatar'
|
||||
| 'id'
|
||||
| 'isArchived'
|
||||
| 'isBlocked'
|
||||
@@ -43,8 +44,10 @@ export type MinimalConversation = Satisfies<
|
||||
| 'profileName'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
>
|
||||
> & {
|
||||
gradientStart?: string;
|
||||
gradientEnd?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export function useMinimalConversation(
|
||||
@@ -59,6 +62,7 @@ export function useMinimalConversation(
|
||||
color,
|
||||
expireTimer,
|
||||
groupVersion,
|
||||
hasAvatar,
|
||||
id,
|
||||
isArchived,
|
||||
isBlocked,
|
||||
@@ -74,10 +78,13 @@ export function useMinimalConversation(
|
||||
profileName,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
} = conversation;
|
||||
return useMemo(() => {
|
||||
const gradientStart = conversation.avatarPlaceholderGradient?.[0];
|
||||
const gradientEnd = conversation.avatarPlaceholderGradient?.[1];
|
||||
return useMemo((): MinimalConversation => {
|
||||
return {
|
||||
gradientStart,
|
||||
gradientEnd,
|
||||
acceptedMessageRequest,
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
@@ -86,6 +93,7 @@ export function useMinimalConversation(
|
||||
color,
|
||||
expireTimer,
|
||||
groupVersion,
|
||||
hasAvatar,
|
||||
id,
|
||||
isArchived,
|
||||
isBlocked,
|
||||
@@ -101,9 +109,10 @@ export function useMinimalConversation(
|
||||
profileName,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
};
|
||||
}, [
|
||||
gradientStart,
|
||||
gradientEnd,
|
||||
acceptedMessageRequest,
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
@@ -112,6 +121,7 @@ export function useMinimalConversation(
|
||||
color,
|
||||
expireTimer,
|
||||
groupVersion,
|
||||
hasAvatar,
|
||||
id,
|
||||
isArchived,
|
||||
isBlocked,
|
||||
@@ -127,6 +137,5 @@ export function useMinimalConversation(
|
||||
profileName,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
]);
|
||||
}
|
||||
|
@@ -44,7 +44,11 @@ export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
|
||||
}
|
||||
|
||||
// Generate correct attributes patch
|
||||
const patch = await applyNewAvatar(newAvatarUrl, attributes, logId);
|
||||
const patch = await applyNewAvatar({
|
||||
newAvatarUrl,
|
||||
attributes,
|
||||
logId,
|
||||
});
|
||||
|
||||
convo.set(patch);
|
||||
await DataWriter.updateConversation(convo.attributes);
|
||||
|
12
ts/model-types.d.ts
vendored
12
ts/model-types.d.ts
vendored
@@ -499,18 +499,12 @@ export type ConversationAttributesType = {
|
||||
isTemporary?: boolean;
|
||||
temporaryMemberCount?: number;
|
||||
|
||||
// Avatars are blurred for some unapproved conversations, but users can manually unblur
|
||||
// them. If the avatar was unblurred and then changed, we don't update this value so
|
||||
// the new avatar gets blurred.
|
||||
//
|
||||
// This value is useless once the message request has been approved. We don't clean it
|
||||
// up but could. We don't persist it but could (though we'd probably want to clean it
|
||||
// up in that case).
|
||||
unblurredAvatarUrl?: string;
|
||||
|
||||
// Legacy field, mapped to above in getConversation()
|
||||
unblurredAvatarPath?: string;
|
||||
|
||||
// remoteAvatarUrl
|
||||
remoteAvatarUrl?: string;
|
||||
|
||||
// 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
|
||||
// if the Chat frame was not imported. That's fine in normal usage, but breaks
|
||||
|
@@ -34,7 +34,6 @@ import {
|
||||
getAvatar,
|
||||
getRawAvatarPath,
|
||||
getLocalAvatarUrl,
|
||||
getLocalProfileAvatarUrl,
|
||||
} from '../util/avatarUtils';
|
||||
import { getDraftPreview } from '../util/getDraftPreview';
|
||||
import { hasDraft } from '../util/hasDraft';
|
||||
@@ -192,6 +191,7 @@ import { getIsInitialContactSync } from '../services/contactSync';
|
||||
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
|
||||
import { cleanupMessages } from '../util/cleanup';
|
||||
import { MessageModel } from './messages';
|
||||
import { applyNewAvatar } from '../groups';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -2485,6 +2485,25 @@ export class ConversationModel extends window.Backbone
|
||||
// time to go through old messages to download attachments.
|
||||
if (didResponseChange && !wasPreviouslyAccepted) {
|
||||
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) {
|
||||
@@ -4922,10 +4941,12 @@ export class ConversationModel extends window.Backbone
|
||||
}
|
||||
}
|
||||
|
||||
async setAndMaybeFetchProfileAvatar(
|
||||
avatarUrl: undefined | null | string,
|
||||
decryptionKey: Uint8Array
|
||||
): Promise<void> {
|
||||
async setAndMaybeFetchProfileAvatar(options: {
|
||||
avatarUrl: undefined | null | string;
|
||||
decryptionKey?: Uint8Array | null | undefined;
|
||||
forceFetch?: boolean;
|
||||
}): Promise<void> {
|
||||
const { avatarUrl, decryptionKey, forceFetch } = options;
|
||||
if (isMe(this.attributes)) {
|
||||
if (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!');
|
||||
}
|
||||
|
||||
if (!this.getAccepted({ ignoreEmptyConvo: true }) && !forceFetch) {
|
||||
this.set({ remoteAvatarUrl: avatarUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
const decrypted = decryptProfile(avatar, decryptionKey);
|
||||
const decrypted = decryptProfile(avatar, updatedDecryptionKey);
|
||||
|
||||
// update the conversation avatar only if hash differs
|
||||
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 {
|
||||
return areWeAdmin(this.attributes);
|
||||
}
|
||||
|
@@ -294,17 +294,16 @@ export class MentionCompletion {
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={member.acceptedMessageRequest}
|
||||
avatarPlaceholderGradient={member.avatarPlaceholderGradient}
|
||||
avatarUrl={member.avatarUrl}
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
conversationType="direct"
|
||||
hasAvatar={member.hasAvatar}
|
||||
i18n={this.options.i18n}
|
||||
isMe={member.isMe}
|
||||
sharedGroupNames={member.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
theme={theme}
|
||||
title={member.title}
|
||||
unblurredAvatarUrl={member.unblurredAvatarUrl}
|
||||
/>
|
||||
<div className="module-composition-input__suggestions__title">
|
||||
<UserText text={member.title} />
|
||||
|
@@ -784,10 +784,10 @@ async function doGetProfile(
|
||||
try {
|
||||
if (requestDecryptionKey != null) {
|
||||
// Note: Fetches avatar
|
||||
await c.setAndMaybeFetchProfileAvatar(
|
||||
profile.avatar,
|
||||
requestDecryptionKey
|
||||
);
|
||||
await c.setAndMaybeFetchProfileAvatar({
|
||||
avatarUrl: profile.avatar,
|
||||
decryptionKey: requestDecryptionKey,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
|
@@ -1743,7 +1743,10 @@ export async function mergeAccountRecord(
|
||||
);
|
||||
|
||||
const avatarUrl = dropNull(accountRecord.avatarUrl);
|
||||
await conversation.setAndMaybeFetchProfileAvatar(avatarUrl, profileKey);
|
||||
await conversation.setAndMaybeFetchProfileAvatar({
|
||||
avatarUrl,
|
||||
decryptionKey: profileKey,
|
||||
});
|
||||
await window.storage.put('avatarUrl', avatarUrl);
|
||||
}
|
||||
|
||||
|
@@ -1655,9 +1655,7 @@ function saveConversation(db: WritableDB, data: ConversationType): void {
|
||||
`
|
||||
).run({
|
||||
id,
|
||||
json: objectToJSON(
|
||||
omit(data, ['profileLastFetchedAt', 'unblurredAvatarUrl'])
|
||||
),
|
||||
json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
|
||||
|
||||
e164: e164 || null,
|
||||
serviceId: serviceId || null,
|
||||
@@ -1723,9 +1721,7 @@ function updateConversation(db: WritableDB, data: ConversationType): void {
|
||||
`
|
||||
).run({
|
||||
id,
|
||||
json: objectToJSON(
|
||||
omit(data, ['profileLastFetchedAt', 'unblurredAvatarUrl'])
|
||||
),
|
||||
json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
|
||||
|
||||
e164: e164 || null,
|
||||
serviceId: serviceId || null,
|
||||
|
@@ -97,6 +97,7 @@ import {
|
||||
getConversationSelector,
|
||||
getMe,
|
||||
getMessagesByConversation,
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import type {
|
||||
@@ -302,8 +303,9 @@ export type ConversationType = ReadonlyDeep<
|
||||
avatarUrl?: string;
|
||||
rawAvatarPath?: string;
|
||||
avatarHash?: string;
|
||||
avatarPlaceholderGradient?: Readonly<[string, string]>;
|
||||
profileAvatarUrl?: string;
|
||||
unblurredAvatarUrl?: string;
|
||||
hasAvatar?: boolean;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
areWePendingApproval?: boolean;
|
||||
@@ -578,6 +580,10 @@ export type ConversationsStateType = ReadonlyDeep<{
|
||||
messagesLookup: MessageLookupType;
|
||||
messagesByConversation: MessagesByConversationType;
|
||||
|
||||
// Map of conversation IDs to a boolean indicating whether an avatar download
|
||||
// was requested
|
||||
pendingRequestedAvatarDownload: Record<string, boolean>;
|
||||
|
||||
preloadData?: ConversationPreloadDataType;
|
||||
}>;
|
||||
|
||||
@@ -640,6 +646,8 @@ export const SET_VOICE_NOTE_PLAYBACK_RATE =
|
||||
'conversations/SET_VOICE_NOTE_PLAYBACK_RATE';
|
||||
export const CONVERSATION_UNLOADED = 'CONVERSATION_UNLOADED';
|
||||
export const SHOW_SPOILER = 'conversations/SHOW_SPOILER';
|
||||
export const SET_PENDING_REQUESTED_AVATAR_DOWNLOAD =
|
||||
'conversations/SET_PENDING_REQUESTED_AVATAR_DOWNLOAD';
|
||||
|
||||
export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{
|
||||
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<{
|
||||
type: 'MESSAGES_ADDED';
|
||||
payload: {
|
||||
@@ -1064,6 +1080,7 @@ export type ConversationActionType =
|
||||
| ReplaceAvatarsActionType
|
||||
| ReviewConversationNameCollisionActionType
|
||||
| ScrollToMessageActionType
|
||||
| SetPendingRequestedAvatarDownloadActionType
|
||||
| TargetedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
| SetComposeGroupExpireTimerActionType
|
||||
@@ -1179,6 +1196,8 @@ export const actions = {
|
||||
saveAvatarToDisk,
|
||||
scrollToMessage,
|
||||
scrollToOldestUnreadMention,
|
||||
setPendingRequestedAvatarDownload,
|
||||
startAvatarDownload,
|
||||
showSpoiler,
|
||||
targetMessage,
|
||||
setAccessControlAddFromInviteLinkSetting,
|
||||
@@ -1220,7 +1239,6 @@ export const actions = {
|
||||
toggleHideStories,
|
||||
toggleSelectMessage,
|
||||
toggleSelectMode,
|
||||
unblurAvatar,
|
||||
updateConversationModelSharedGroups,
|
||||
updateGroupAttributes,
|
||||
updateLastMessage,
|
||||
@@ -1465,19 +1483,7 @@ function removeMember(
|
||||
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 {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
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
|
||||
|
||||
export function getEmptyState(): ConversationsStateType {
|
||||
@@ -4892,6 +4965,7 @@ export function getEmptyState(): ConversationsStateType {
|
||||
selectedMessageIds: undefined,
|
||||
showArchived: false,
|
||||
hasContactSpoofingReview: false,
|
||||
pendingRequestedAvatarDownload: {},
|
||||
targetedConversationPanels: {
|
||||
isAnimating: 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;
|
||||
}
|
||||
|
@@ -1339,3 +1339,14 @@ export const getPreloadedConversationId = createSelector(
|
||||
getConversations,
|
||||
({ preloadData }): string | undefined => preloadData?.conversationId
|
||||
);
|
||||
|
||||
export const getPendingAvatarDownloadSelector = createSelector(
|
||||
getConversations,
|
||||
(conversations: ConversationsStateType) => {
|
||||
return (conversationId: string): boolean => {
|
||||
return Boolean(
|
||||
conversations.pendingRequestedAvatarDownload[conversationId]
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -164,7 +164,6 @@ type FormattedContact = Partial<ConversationType> &
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
export type PropsForMessage = Omit<TimelineMessagePropsData, 'interactionMode'>;
|
||||
export type MessagePropsType = Omit<
|
||||
@@ -351,11 +350,13 @@ const getAuthorForMessage = (
|
||||
options: GetContactOptions
|
||||
): PropsData['author'] => {
|
||||
const {
|
||||
avatarPlaceholderGradient,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badges,
|
||||
color,
|
||||
firstName,
|
||||
hasAvatar,
|
||||
id,
|
||||
isMe,
|
||||
name,
|
||||
@@ -363,15 +364,16 @@ const getAuthorForMessage = (
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
} = getContact(message, options);
|
||||
|
||||
const unsafe = {
|
||||
avatarPlaceholderGradient,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
badges,
|
||||
color,
|
||||
firstName,
|
||||
hasAvatar,
|
||||
id,
|
||||
isMe,
|
||||
name,
|
||||
@@ -379,7 +381,6 @@ const getAuthorForMessage = (
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarUrl,
|
||||
};
|
||||
|
||||
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;
|
||||
|
@@ -166,11 +166,13 @@ export function getStoryView(
|
||||
const sender = pick(
|
||||
conversationSelector(story.sourceServiceId || story.source),
|
||||
[
|
||||
'avatarPlaceholderGradient',
|
||||
'acceptedMessageRequest',
|
||||
'avatarUrl',
|
||||
'badges',
|
||||
'color',
|
||||
'firstName',
|
||||
'hasAvatar',
|
||||
'hideStory',
|
||||
'id',
|
||||
'isMe',
|
||||
|
@@ -6,7 +6,10 @@ import { AboutContactModal } from '../../components/conversation/AboutContactMod
|
||||
import { isSignalConnection } from '../../util/getSignalConnections';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getGlobalModalsState } from '../selectors/globalModals';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
@@ -35,8 +38,9 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
const globalModals = useSelector(getGlobalModalsState);
|
||||
const { aboutContactModalContactId: contactId } = globalModals;
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
|
||||
|
||||
const { updateSharedGroups, unblurAvatar } = useConversationsActions();
|
||||
const { startAvatarDownload, updateSharedGroups } = useConversationsActions();
|
||||
|
||||
const conversation = getConversation(contactId);
|
||||
const { id: conversationId } = conversation ?? {};
|
||||
@@ -63,7 +67,6 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
unblurAvatar={unblurAvatar}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||
isSignalConnection={isSignalConnection(conversation)}
|
||||
@@ -71,6 +74,12 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
onClose={toggleAboutContactModal}
|
||||
onOpenNotePreviewModal={handleOpenNotePreviewModal}
|
||||
toggleProfileNameWarningModal={toggleProfileNameWarningModal}
|
||||
pendingAvatarDownload={
|
||||
conversationId ? isPendingAvatarDownload(conversationId) : false
|
||||
}
|
||||
startAvatarDownload={
|
||||
conversationId ? () => startAvatarDownload(conversationId) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@@ -54,6 +54,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
|
||||
updateConversationModelSharedGroups,
|
||||
toggleAdmin,
|
||||
blockConversation,
|
||||
startAvatarDownload,
|
||||
} = useConversationsActions();
|
||||
const { viewUserStories } = useStoriesActions();
|
||||
const {
|
||||
@@ -94,6 +95,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
removeMemberFromGroup={removeMemberFromGroup}
|
||||
showConversation={showConversation}
|
||||
startAvatarDownload={() => startAvatarDownload(contact.id)}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
getAllComposableConversations,
|
||||
getConversationByIdSelector,
|
||||
getConversationByServiceIdSelector,
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations';
|
||||
import {
|
||||
getAreWeASubscriber,
|
||||
@@ -98,6 +99,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
const conversationSelector = useSelector(getConversationByIdSelector);
|
||||
const defaultConversationColor = useSelector(getDefaultConversationColor);
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
|
||||
const selectedNavTab = useSelector(getSelectedNavTab);
|
||||
|
||||
const {
|
||||
@@ -114,6 +116,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
setDisappearingMessages,
|
||||
setMuteExpiration,
|
||||
showConversation,
|
||||
startAvatarDownload,
|
||||
updateGroupAttributes,
|
||||
updateNicknameAndNote,
|
||||
} = useConversationsActions();
|
||||
@@ -205,6 +208,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
pendingApprovalMemberships={pendingApprovalMemberships}
|
||||
pendingAvatarDownload={isPendingAvatarDownload(conversationId)}
|
||||
pendingMemberships={pendingMemberships}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
renderChooseGroupMembersModal={renderChooseGroupMembersModal}
|
||||
@@ -218,6 +222,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
showContactModal={showContactModal}
|
||||
showConversation={showConversation}
|
||||
showLightbox={showLightbox}
|
||||
startAvatarDownload={() => startAvatarDownload(conversationId)}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
|
||||
|
@@ -8,7 +8,10 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { getHasStoriesSelector } from '../selectors/stories2';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations';
|
||||
import {
|
||||
type ConversationType,
|
||||
useConversationsActions,
|
||||
@@ -46,6 +49,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const hasStoriesSelector = useSelector(getHasStoriesSelector);
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
|
||||
const conversation = conversationSelector(id);
|
||||
if (conversation == null) {
|
||||
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 fromOrAddedByTrustedContact =
|
||||
isFromOrAddedByTrustedContact(conversation);
|
||||
const { pushPanelForConversation, unblurAvatar, updateSharedGroups } =
|
||||
const { pushPanelForConversation, startAvatarDownload, updateSharedGroups } =
|
||||
useConversationsActions();
|
||||
const { toggleAboutContactModal, toggleProfileNameWarningModal } =
|
||||
useGlobalModalActions();
|
||||
@@ -64,10 +68,12 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
}, [pushPanelForConversation]);
|
||||
const { viewUserStories } = useStoriesActions();
|
||||
const {
|
||||
avatarPlaceholderGradient,
|
||||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarUrl,
|
||||
groupDescription,
|
||||
hasAvatar,
|
||||
isMe,
|
||||
membersCount,
|
||||
nicknameGivenName,
|
||||
@@ -77,7 +83,6 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
sharedGroupNames,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarUrl,
|
||||
} = conversation;
|
||||
|
||||
const isDirectConvoAndHasNickname =
|
||||
@@ -85,6 +90,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
|
||||
return (
|
||||
<ConversationHero
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
about={about}
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarUrl={avatarUrl}
|
||||
@@ -92,6 +98,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
conversationType={type}
|
||||
fromOrAddedByTrustedContact={fromOrAddedByTrustedContact}
|
||||
groupDescription={groupDescription}
|
||||
hasAvatar={hasAvatar}
|
||||
hasStories={hasStories}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
@@ -100,15 +107,15 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
isSignalConversation={isSignalConversationValue}
|
||||
membersCount={membersCount}
|
||||
openConversationDetails={openConversationDetails}
|
||||
pendingAvatarDownload={isPendingAvatarDownload(id)}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
startAvatarDownload={() => startAvatarDownload(id)}
|
||||
theme={theme}
|
||||
title={title}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
toggleProfileNameWarningModal={toggleProfileNameWarningModal}
|
||||
unblurAvatar={unblurAvatar}
|
||||
unblurredAvatarUrl={unblurredAvatarUrl}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
viewUserStories={viewUserStories}
|
||||
/>
|
||||
|
@@ -12,6 +12,7 @@ import type { GroupListItemConversationType } from '../../components/conversatio
|
||||
import { getRandomColor } from './getRandomColor';
|
||||
import { ConversationColors } from '../../types/Colors';
|
||||
import { StorySendMode } from '../../types/Stories';
|
||||
import { getAvatarPlaceholderGradient } from '../../utils/getAvatarPlaceholderGradient';
|
||||
|
||||
export const getAvatarPath = (): string =>
|
||||
sample([
|
||||
@@ -27,6 +28,7 @@ export function getDefaultConversation(
|
||||
const lastName = casual.last_name;
|
||||
|
||||
return {
|
||||
avatarPlaceholderGradient: getAvatarPlaceholderGradient(0),
|
||||
acceptedMessageRequest: true,
|
||||
avatarUrl: getAvatarPath(),
|
||||
badges: [],
|
||||
|
@@ -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: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
@@ -6,7 +6,11 @@ import { assert } from 'chai';
|
||||
import * as durations from '../../util/durations';
|
||||
import type { App, Bootstrap } 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) {
|
||||
this.timeout(durations.MINUTE);
|
||||
@@ -50,7 +54,6 @@ describe('storage service', function (this: Mocha.Suite) {
|
||||
const window = await app.getWindow();
|
||||
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||
|
||||
debug('Opening conversation with a stranger');
|
||||
debug(stranger.toContact().aci);
|
||||
@@ -77,14 +80,7 @@ describe('storage service', function (this: Mocha.Suite) {
|
||||
}
|
||||
|
||||
debug('Accept conversation from a stranger');
|
||||
await conversationStack
|
||||
.locator('.module-message-request-actions button >> "Accept"')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.MessageRequestActionsConfirmation')
|
||||
.getByRole('button', { name: 'Accept' })
|
||||
.click();
|
||||
await acceptConversation(window);
|
||||
|
||||
debug('Verify that storage state was updated');
|
||||
{
|
||||
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
@@ -233,6 +233,8 @@ export type StorageAccessType = {
|
||||
|
||||
postRegistrationSyncsStatus: 'incomplete' | 'complete';
|
||||
|
||||
avatarsHaveBeenMigrated: boolean;
|
||||
|
||||
// Deprecated
|
||||
'challenge:retry-message-ids': never;
|
||||
nextSignedKeyRotationTime: number;
|
||||
|
@@ -82,11 +82,13 @@ export type StoryViewType = {
|
||||
readAt?: number;
|
||||
sender: Pick<
|
||||
ConversationType,
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarUrl'
|
||||
| 'badges'
|
||||
| 'color'
|
||||
| 'firstName'
|
||||
| 'hasAvatar'
|
||||
| 'hideStory'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
|
@@ -7,6 +7,14 @@ import { isMe } from './whatTypeOfConversation';
|
||||
import { isSignalConversation } from './isSignalConversation';
|
||||
import { getLocalAttachmentUrl } from './getLocalAttachmentUrl';
|
||||
|
||||
export function hasAvatar(
|
||||
conversationAttrs: ConversationAttributesType
|
||||
): boolean {
|
||||
return Boolean(
|
||||
getAvatar(conversationAttrs) || conversationAttrs.remoteAvatarUrl
|
||||
);
|
||||
}
|
||||
|
||||
export function getAvatarHash(
|
||||
conversationAttrs: ConversationAttributesType
|
||||
): undefined | string {
|
||||
@@ -65,29 +73,3 @@ export function getLocalProfileAvatarUrl(
|
||||
const avatar = conversationAttrs.profileAvatar || conversationAttrs.avatar;
|
||||
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 });
|
||||
}
|
||||
|
@@ -17,11 +17,11 @@ import { canEditGroupInfo } from './canEditGroupInfo';
|
||||
import { dropNull } from './dropNull';
|
||||
import { getAboutText } from './getAboutText';
|
||||
import {
|
||||
getLocalUnblurredAvatarUrl,
|
||||
getAvatarHash,
|
||||
getLocalAvatarUrl,
|
||||
getLocalProfileAvatarUrl,
|
||||
getRawAvatarPath,
|
||||
hasAvatar,
|
||||
} from './avatarUtils';
|
||||
import { getAvatarData } from './getAvatarData';
|
||||
import { getConversationMembers } from './getConversationMembers';
|
||||
@@ -46,16 +46,17 @@ import {
|
||||
isMe,
|
||||
} from './whatTypeOfConversation';
|
||||
import {
|
||||
areWePending,
|
||||
getBannedMemberships,
|
||||
getMembersCount,
|
||||
getMemberships,
|
||||
getPendingApprovalMemberships,
|
||||
getPendingMemberships,
|
||||
isMember,
|
||||
isMemberAwaitingApproval,
|
||||
isMemberPending,
|
||||
} from './groupMembershipUtils';
|
||||
import { isNotNil } from './isNotNil';
|
||||
import { getIdentifierHash } from '../Crypto';
|
||||
import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient';
|
||||
|
||||
const EMPTY_ARRAY: Readonly<[]> = [];
|
||||
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
|
||||
@@ -88,7 +89,13 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
);
|
||||
|
||||
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, {
|
||||
aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
|
||||
@@ -97,6 +104,11 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
groupId: attributes.groupId,
|
||||
});
|
||||
|
||||
const avatarPlaceholderGradient =
|
||||
hasAvatar(attributes) && identifierHash != null
|
||||
? getAvatarPlaceholderGradient(identifierHash)
|
||||
: undefined;
|
||||
|
||||
const { draftTimestamp, draftEditMessage, timestamp } = attributes;
|
||||
const draftPreview = getDraftPreview(attributes);
|
||||
const draftText = dropNull(attributes.draft);
|
||||
@@ -142,20 +154,14 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
acceptedMessageRequest: isConversationAccepted(attributes),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
activeAt: attributes.active_at!,
|
||||
areWePending:
|
||||
ourAci &&
|
||||
(isMemberPending(attributes, ourAci) ||
|
||||
Boolean(
|
||||
ourPni &&
|
||||
!isMember(attributes, ourAci) &&
|
||||
isMemberPending(attributes, ourPni)
|
||||
)),
|
||||
areWePending: areWePending(attributes),
|
||||
areWePendingApproval: Boolean(
|
||||
ourConversationId &&
|
||||
ourAci &&
|
||||
isMemberAwaitingApproval(attributes, ourAci)
|
||||
),
|
||||
areWeAdmin: areWeAdmin(attributes),
|
||||
avatarPlaceholderGradient,
|
||||
avatars: getAvatarData(attributes),
|
||||
badges: attributes.badges ?? EMPTY_ARRAY,
|
||||
canChangeTimer: canChangeTimer(attributes),
|
||||
@@ -164,8 +170,8 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
avatarUrl: getLocalAvatarUrl(attributes),
|
||||
rawAvatarPath: getRawAvatarPath(attributes),
|
||||
avatarHash: getAvatarHash(attributes),
|
||||
unblurredAvatarUrl: getLocalUnblurredAvatarUrl(attributes),
|
||||
profileAvatarUrl: getLocalProfileAvatarUrl(attributes),
|
||||
hasAvatar: hasAvatar(attributes),
|
||||
color,
|
||||
conversationColor: attributes.conversationColor,
|
||||
customColor,
|
||||
|
@@ -184,3 +184,22 @@ export function getMemberships(
|
||||
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)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
33
ts/utils/getAvatarPlaceholderGradient.ts
Normal file
33
ts/utils/getAvatarPlaceholderGradient.ts
Normal 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];
|
||||
}
|
Reference in New Issue
Block a user