diff --git a/app/containers/Avatar/Avatar.tsx b/app/containers/Avatar/Avatar.tsx
index f5da333dc0e..f8372d59337 100644
--- a/app/containers/Avatar/Avatar.tsx
+++ b/app/containers/Avatar/Avatar.tsx
@@ -1,15 +1,13 @@
import { memo } from 'react';
import { View } from 'react-native';
-import { Image } from 'expo-image';
-import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Emoji from '../markdown/components/emoji/Emoji';
import { getAvatarURL } from '~/lib/methods/helpers/getAvatarUrl';
-import { headers } from '~/lib/methods/helpers/fetch';
import { SubscriptionType } from '~/definitions';
import { type IAvatar } from './interfaces';
import I18n from '~/i18n';
import Touch from '../Touch';
+import AvatarImage from './AvatarImage';
import { AVATAR_BORDER_RADIUS } from './constants';
const Avatar = memo(
@@ -78,16 +76,7 @@ const Avatar = memo(
});
}
- image = (
-
- );
+ image = ;
}
if (onPress) {
diff --git a/app/containers/Avatar/AvatarImage.tsx b/app/containers/Avatar/AvatarImage.tsx
new file mode 100644
index 00000000000..ca8fba0e361
--- /dev/null
+++ b/app/containers/Avatar/AvatarImage.tsx
@@ -0,0 +1,40 @@
+import { useState } from 'react';
+import { Image } from 'expo-image';
+import { settings as RocketChatSettings } from '@rocket.chat/sdk';
+
+import { headers } from '~/lib/methods/helpers/fetch';
+import { useTheme } from '~/theme';
+import AvatarSkeleton from './AvatarSkeleton';
+
+interface IAvatarImage {
+ uri?: string;
+ size: number;
+ borderRadius: number;
+}
+
+const AvatarImage = ({ uri, size, borderRadius }: IAvatarImage) => {
+ const { colors } = useTheme();
+ const [finishedUri, setFinishedUri] = useState();
+ const showSkeleton = !!uri && finishedUri !== uri;
+
+ const onFinish = () => setFinishedUri(uri);
+
+ return (
+ <>
+
+ {showSkeleton ? : null}
+ >
+ );
+};
+
+export default AvatarImage;
diff --git a/app/containers/Avatar/AvatarSkeleton.tsx b/app/containers/Avatar/AvatarSkeleton.tsx
new file mode 100644
index 00000000000..448f91074b1
--- /dev/null
+++ b/app/containers/Avatar/AvatarSkeleton.tsx
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
+
+import { useTheme } from '~/theme';
+import { SKELETON_DELAY } from './constants';
+
+interface IAvatarSkeleton {
+ size: number;
+ borderRadius: number;
+}
+
+const AvatarSkeleton = ({ size, borderRadius }: IAvatarSkeleton) => {
+ const { colors } = useTheme();
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setVisible(true), SKELETON_DELAY);
+ return () => clearTimeout(timer);
+ }, []);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default AvatarSkeleton;
diff --git a/app/containers/Avatar/Avatar.test.tsx b/app/containers/Avatar/__tests__/Avatar.test.tsx
similarity index 68%
rename from app/containers/Avatar/Avatar.test.tsx
rename to app/containers/Avatar/__tests__/Avatar.test.tsx
index d86986bbf56..498792bbc1d 100644
--- a/app/containers/Avatar/Avatar.test.tsx
+++ b/app/containers/Avatar/__tests__/Avatar.test.tsx
@@ -1,4 +1,4 @@
import { generateSnapshots } from '~/.rnstorybook/generateSnapshots';
-import * as stories from './Avatar.stories';
+import * as stories from '../Avatar.stories';
generateSnapshots(stories);
diff --git a/app/containers/Avatar/__tests__/AvatarImage.test.tsx b/app/containers/Avatar/__tests__/AvatarImage.test.tsx
new file mode 100644
index 00000000000..9a14884139c
--- /dev/null
+++ b/app/containers/Avatar/__tests__/AvatarImage.test.tsx
@@ -0,0 +1,68 @@
+import { act, fireEvent, render } from '@testing-library/react-native';
+import { Image } from 'expo-image';
+
+import AvatarImage from '../AvatarImage';
+import { SKELETON_DELAY } from '../constants';
+
+const showDelay = () => act(() => jest.advanceTimersByTime(SKELETON_DELAY));
+
+beforeEach(() => jest.useFakeTimers());
+afterEach(() => jest.useRealTimers());
+
+describe('AvatarImage', () => {
+ it('shows the skeleton until the image loads', () => {
+ const { getByTestId, queryByTestId, UNSAFE_getByType } = render(
+
+ );
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ showDelay();
+ expect(getByTestId('avatar-skeleton')).toBeTruthy();
+ fireEvent(UNSAFE_getByType(Image), 'load');
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ });
+
+ it('hides the skeleton when the image fails to load', () => {
+ const { queryByTestId, UNSAFE_getByType } = render();
+ showDelay();
+ fireEvent(UNSAFE_getByType(Image), 'error');
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ });
+
+ it('restarts the delay when the uri changes after a load', () => {
+ const { queryByTestId, rerender, UNSAFE_getByType } = render(
+
+ );
+ showDelay();
+ fireEvent(UNSAFE_getByType(Image), 'load');
+ rerender();
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ showDelay();
+ expect(queryByTestId('avatar-skeleton')).toBeTruthy();
+ });
+
+ it('keeps the skeleton when the uri changes before the image loads', () => {
+ const { getByTestId, rerender } = render();
+ showDelay();
+ rerender();
+ expect(getByTestId('avatar-skeleton')).toBeTruthy();
+ });
+
+ it('hides the skeleton when the uri is removed after a load', () => {
+ const { queryByTestId, rerender, UNSAFE_getByType } = render(
+
+ );
+ fireEvent(UNSAFE_getByType(Image), 'load');
+ rerender();
+ showDelay();
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ });
+
+ it('never shows the skeleton when the image loads before the delay', () => {
+ const { queryByTestId, UNSAFE_getByType } = render();
+ act(() => jest.advanceTimersByTime(SKELETON_DELAY - 1));
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ fireEvent(UNSAFE_getByType(Image), 'load');
+ showDelay();
+ expect(queryByTestId('avatar-skeleton')).toBeNull();
+ });
+});
diff --git a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap b/app/containers/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
similarity index 67%
rename from app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap
rename to app/containers/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
index 7f4e3e01109..0ad181b0981 100644
--- a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap
+++ b/app/containers/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
@@ -17,6 +17,7 @@ exports[`Story Snapshots: AvatarBase64 should match snapshot 1`] = `
testID="avatar"
>