Keep email participant avatar colors consistent (#23444)
## Summary - Fixes issues where the same record didn't share the same avatar in multiple places of the inbox. - Add a shared avatar color seed resolver for email participants. - Apply consistent placeholder colors across participant chips and email thread previews. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23444?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -2,6 +2,7 @@ import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
|
||||
import { getDisplayNameFromParticipant } from '@/activities/emails/utils/getDisplayNameFromParticipant';
|
||||
import { getEmailParticipantAvatarColorSeed } from '@/activities/emails/utils/getEmailParticipantAvatarColorSeed';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
@@ -73,6 +74,9 @@ export const ParticipantChip = ({
|
||||
avatarUrl={getAbsoluteImageUrl(avatarUrl)}
|
||||
type="rounded"
|
||||
placeholder={displayName}
|
||||
placeholderColorSeed={getEmailParticipantAvatarColorSeed(
|
||||
participant,
|
||||
)}
|
||||
size="sm"
|
||||
/>
|
||||
</StyledAvatarContainer>
|
||||
|
||||
+40
-18
@@ -2,6 +2,7 @@ import { styled } from '@linaria/react';
|
||||
|
||||
import { ActivityRow } from '@/activities/components/ActivityRow';
|
||||
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
|
||||
import { getEmailParticipantAvatarColorSeed } from '@/activities/emails/utils/getEmailParticipantAvatarColorSeed';
|
||||
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
||||
import { useContext } from 'react';
|
||||
|
||||
@@ -78,6 +79,13 @@ type EmailThreadPreviewProps = {
|
||||
thread: TimelineThread;
|
||||
};
|
||||
|
||||
type LastAvatar = {
|
||||
displayedName: string | undefined;
|
||||
avatarUrl: string | undefined;
|
||||
isCountIcon: boolean;
|
||||
placeholderColorSeed: string | undefined;
|
||||
};
|
||||
|
||||
export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { openRecordInSidePanel } = useOpenRecordInSidePanel();
|
||||
@@ -93,14 +101,29 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
? `, ${thread.lastTwoParticipants?.[1]?.displayName}`
|
||||
: '');
|
||||
|
||||
const [finalDisplayedName, finalAvatarUrl, isCountIcon] =
|
||||
const lastParticipant = thread?.lastTwoParticipants?.[1];
|
||||
|
||||
const {
|
||||
displayedName,
|
||||
avatarUrl,
|
||||
isCountIcon,
|
||||
placeholderColorSeed,
|
||||
}: LastAvatar =
|
||||
thread.participantCount > 3
|
||||
? [`${thread.participantCount}`, '', true]
|
||||
: [
|
||||
thread?.lastTwoParticipants?.[1]?.displayName,
|
||||
thread?.lastTwoParticipants?.[1]?.avatarUrl,
|
||||
false,
|
||||
];
|
||||
? {
|
||||
displayedName: `${thread.participantCount}`,
|
||||
avatarUrl: '',
|
||||
isCountIcon: true,
|
||||
placeholderColorSeed: undefined,
|
||||
}
|
||||
: {
|
||||
displayedName: lastParticipant?.displayName,
|
||||
avatarUrl: lastParticipant?.avatarUrl,
|
||||
isCountIcon: false,
|
||||
placeholderColorSeed: isDefined(lastParticipant)
|
||||
? getEmailParticipantAvatarColorSeed(lastParticipant)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const handleThreadClick = () => {
|
||||
const canOpen =
|
||||
@@ -122,10 +145,9 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(thread?.firstParticipant?.avatarUrl)}
|
||||
placeholder={thread.firstParticipant.displayName}
|
||||
placeholderColorSeed={
|
||||
thread.firstParticipant.workspaceMemberId ||
|
||||
thread.firstParticipant.personId
|
||||
}
|
||||
placeholderColorSeed={getEmailParticipantAvatarColorSeed(
|
||||
thread.firstParticipant,
|
||||
)}
|
||||
type="rounded"
|
||||
/>
|
||||
{isDefined(thread?.lastTwoParticipants?.[0]) && (
|
||||
@@ -135,19 +157,19 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
thread.lastTwoParticipants[0].avatarUrl,
|
||||
)}
|
||||
placeholder={thread.lastTwoParticipants[0].displayName}
|
||||
placeholderColorSeed={
|
||||
thread.lastTwoParticipants[0].workspaceMemberId ||
|
||||
thread.lastTwoParticipants[0].personId
|
||||
}
|
||||
placeholderColorSeed={getEmailParticipantAvatarColorSeed(
|
||||
thread.lastTwoParticipants[0],
|
||||
)}
|
||||
type="rounded"
|
||||
/>
|
||||
</StyledAvatarWrapper>
|
||||
)}
|
||||
{finalDisplayedName && (
|
||||
{displayedName && (
|
||||
<StyledAvatarWrapper>
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(finalAvatarUrl)}
|
||||
placeholder={finalDisplayedName}
|
||||
avatarUrl={getAbsoluteImageUrl(avatarUrl)}
|
||||
placeholder={displayedName}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
type="rounded"
|
||||
color={isCountIcon ? theme.grayScale.gray11 : undefined}
|
||||
backgroundColor={
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { getEmailParticipantAvatarColorSeed } from '@/activities/emails/utils/getEmailParticipantAvatarColorSeed';
|
||||
|
||||
describe('getEmailParticipantAvatarColorSeed', () => {
|
||||
it('uses the person ID consistently across participant shapes', () => {
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
personId: 'person-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
}),
|
||||
).toBe('person-id');
|
||||
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
person: { id: 'person-id' },
|
||||
workspaceMember: { id: 'workspace-member-id' },
|
||||
}),
|
||||
).toBe('person-id');
|
||||
});
|
||||
|
||||
it('falls back to the workspace member ID', () => {
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
}),
|
||||
).toBe('workspace-member-id');
|
||||
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
workspaceMember: { id: 'workspace-member-id' },
|
||||
}),
|
||||
).toBe('workspace-member-id');
|
||||
});
|
||||
|
||||
it('falls back to the normalized handle, then the display name', () => {
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
handle: ' Person@Example.com ',
|
||||
displayName: 'Person',
|
||||
}),
|
||||
).toBe('person@example.com');
|
||||
|
||||
expect(
|
||||
getEmailParticipantAvatarColorSeed({
|
||||
displayName: 'Person',
|
||||
}),
|
||||
).toBe('Person');
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
type EmailParticipantAvatarColorSeedInput = {
|
||||
personId?: string | null;
|
||||
workspaceMemberId?: string | null;
|
||||
person?: { id: string } | null;
|
||||
workspaceMember?: { id: string } | null;
|
||||
handle?: string | null;
|
||||
displayName?: string | null;
|
||||
};
|
||||
|
||||
export const getEmailParticipantAvatarColorSeed = (
|
||||
participant: EmailParticipantAvatarColorSeedInput,
|
||||
) =>
|
||||
participant.personId ||
|
||||
participant.person?.id ||
|
||||
participant.workspaceMemberId ||
|
||||
participant.workspaceMember?.id ||
|
||||
participant.handle?.trim().toLowerCase() ||
|
||||
participant.displayName ||
|
||||
'';
|
||||
Reference in New Issue
Block a user