Sanitize url before fetching favicon and display letter avatar if it can't be retrieved (#1035)

* Sanitize url before fetching favicon and display letter avatar if it can't be retrieved

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>

* Priorotise www for apple.com domain

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>

* Add requested changes

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>

* Fix the tests

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>

* Change avatar generation strategy

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>

---------

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>
This commit is contained in:
gitstart-twenty 2023-08-03 02:53:56 +08:00 committed by GitHub
parent bfd748e175
commit 2680289ff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 20 deletions

View File

@ -8,6 +8,7 @@ import {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldURLMetadata, ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField'; } from '@/ui/table/types/ViewField';
import { sanitizeURL } from '~/utils';
import { GenericEditableURLCellEditMode } from './GenericEditableURLCellEditMode'; import { GenericEditableURLCellEditMode } from './GenericEditableURLCellEditMode';
@ -33,7 +34,9 @@ export function GenericEditableURLCell({
<EditableCell <EditableCell
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={<GenericEditableURLCellEditMode viewField={viewField} />} editModeContent={<GenericEditableURLCellEditMode viewField={viewField} />}
nonEditModeContent={<InplaceInputURLDisplayMode value={fieldValue} />} nonEditModeContent={
<InplaceInputURLDisplayMode value={sanitizeURL(fieldValue)} />
}
></EditableCell> ></EditableCell>
); );
} }

View File

@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '~/utils/isNonEmptyString'; import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -86,6 +87,20 @@ export function Avatar({
type = 'squared', type = 'squared',
}: OwnProps) { }: OwnProps) {
const noAvatarUrl = !isNonEmptyString(avatarUrl); const noAvatarUrl = !isNonEmptyString(avatarUrl);
const [isInvalidAvatarUrl, setIsInvalidAvatarUrl] = useState(false);
useEffect(() => {
if (avatarUrl) {
new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(false);
img.onerror = () => resolve(true);
img.src = getImageAbsoluteURIOrBase64(avatarUrl) as string;
}).then((res) => {
setIsInvalidAvatarUrl(res as boolean);
});
}
}, [avatarUrl]);
return ( return (
<StyledAvatar <StyledAvatar
@ -95,7 +110,8 @@ export function Avatar({
type={type} type={type}
colorId={colorId} colorId={colorId}
> >
{noAvatarUrl && placeholder[0]?.toLocaleUpperCase()} {(noAvatarUrl || isInvalidAvatarUrl) &&
placeholder[0]?.toLocaleUpperCase()}
</StyledAvatar> </StyledAvatar>
); );
} }

View File

@ -1,21 +1,49 @@
import { getLogoUrlFromDomainName } from '..'; import { getLogoUrlFromDomainName, sanitizeURL } from '..';
describe('sanitizeURL', () => {
test('should sanitize the URL correctly', () => {
expect(sanitizeURL('http://example.com/')).toBe('example.com');
expect(sanitizeURL('https://www.example.com/')).toBe('example.com');
expect(sanitizeURL('www.example.com')).toBe('example.com');
expect(sanitizeURL('example.com')).toBe('example.com');
expect(sanitizeURL('example.com/')).toBe('example.com');
});
test('should handle undefined input', () => {
expect(sanitizeURL(undefined)).toBe('');
});
});
describe('getLogoUrlFromDomainName', () => { describe('getLogoUrlFromDomainName', () => {
it(`should generate logo url if undefined `, () => { test('should return the correct logo URL for a given domain', () => {
expect(getLogoUrlFromDomainName('example.com')).toBe(
'https://favicon.twenty.com/example.com',
);
expect(getLogoUrlFromDomainName('http://example.com/')).toBe(
'https://favicon.twenty.com/example.com',
);
expect(getLogoUrlFromDomainName('https://www.example.com/')).toBe(
'https://favicon.twenty.com/example.com',
);
expect(getLogoUrlFromDomainName('www.example.com')).toBe(
'https://favicon.twenty.com/example.com',
);
expect(getLogoUrlFromDomainName('example.com/')).toBe(
'https://favicon.twenty.com/example.com',
);
expect(getLogoUrlFromDomainName('apple.com')).toBe(
'https://favicon.twenty.com/apple.com',
);
});
test('should handle undefined input', () => {
expect(getLogoUrlFromDomainName(undefined)).toBe( expect(getLogoUrlFromDomainName(undefined)).toBe(
'https://api.faviconkit.com/undefined/144', 'https://favicon.twenty.com/',
);
});
it(`should generate logo url if defined `, () => {
expect(getLogoUrlFromDomainName('test.com')).toBe(
'https://api.faviconkit.com/test.com/144',
);
});
it(`should generate logo url if empty `, () => {
expect(getLogoUrlFromDomainName('')).toBe(
'https://api.faviconkit.com//144',
); );
}); });
}); });

View File

@ -10,6 +10,15 @@ export function formatToHumanReadableDate(date: Date | string) {
}).format(parsedJSDate); }).format(parsedJSDate);
} }
export const getLogoUrlFromDomainName = (domainName?: string): string => { export function sanitizeURL(link: string | null | undefined) {
return `https://api.faviconkit.com/${domainName}/144`; return link
}; ? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
: '';
}
export function getLogoUrlFromDomainName(
domainName?: string,
): string | undefined {
const sanitizedDomain = sanitizeURL(domainName);
return `https://favicon.twenty.com/${sanitizedDomain}`;
}