Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/views/WorkspaceView/ServerAvatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import { themes } from '../../lib/constants/colors';
import ServerAvatar from './ServerAvatar';
import { ThemeContext, type TSupportedThemes } from '../../theme';

export default {
title: 'WorkspaceView/ServerAvatar'
};

const BASE_URL = 'https://open.rocket.chat';

const ThemedServerAvatar = ({
url = BASE_URL,
image,
theme = 'light'
}: {
url?: string;
image?: string;
theme?: TSupportedThemes;
}) => (
<ThemeContext.Provider
value={{
theme,
colors: themes[theme]
}}>
<ServerAvatar url={url} image={image} />
</ThemeContext.Provider>
);

export const WithImage = () => <ThemedServerAvatar image='images/logo/android-chrome-512x512.png' />;

export const WithoutImage = () => <ThemedServerAvatar />;

export const WithEmptyImage = () => <ThemedServerAvatar image='' />;

export const Themes = () => (
<>
<ThemedServerAvatar image='images/logo/android-chrome-512x512.png' theme='light' />
<ThemedServerAvatar image='images/logo/android-chrome-512x512.png' theme='dark' />
<ThemedServerAvatar image='images/logo/android-chrome-512x512.png' theme='black' />
</>
);

export const SkeletonThemes = () => (
<>
<ThemedServerAvatar theme='light' />
<ThemedServerAvatar theme='dark' />
<ThemedServerAvatar theme='black' />
</>
);
4 changes: 4 additions & 0 deletions app/views/WorkspaceView/ServerAvatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots';
import * as stories from './ServerAvatar.stories';

generateSnapshots(stories);
12 changes: 9 additions & 3 deletions app/views/WorkspaceView/ServerAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Image } from 'expo-image';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';

import { isTablet } from '../../lib/methods/helpers';
import { useTheme } from '../../theme';
Expand All @@ -26,16 +27,21 @@ const styles = StyleSheet.create({

interface IServerAvatar {
url: string;
image: string;
image?: string;
}

// TODO: missing skeleton
const ServerAvatar = React.memo(({ url, image }: IServerAvatar) => {
const { colors } = useTheme();

return (
<View style={styles.container}>
{image && <Image style={[styles.image, { borderColor: colors.strokeLight }]} source={{ uri: `${url}/${image}` }} />}
{image ? (
<Image style={[styles.image, { borderColor: colors.strokeLight }]} source={{ uri: `${url}/${image}` }} />
) : (
<SkeletonPlaceholder backgroundColor={colors.surfaceNeutral} borderRadius={BORDER_RADIUS}>
<SkeletonPlaceholder.Item width={SIZE} height={SIZE} />
</SkeletonPlaceholder>
)}
Comment on lines +38 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the component and relevant lines
FILE="app/views/WorkspaceView/ServerAvatar.tsx"
sed -n '1,200p' "$FILE" | nl -ba | sed -n '1,200p'

# Specifically show the referenced range (38-44) with context
sed -n '25,70p' "$FILE" | nl -ba | sed -n '1,120p'

# Search for any existing image loading/error handling in this file
rg -n "onLoad(Start|End)|onError|isImageLoading|hasImageError|SkeletonPlaceholder" "$FILE" || true

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/views/WorkspaceView/ServerAvatar.tsx"

# Show file with line numbers using awk (since nl isn't available)
awk '{print NR "\t" $0}' "$FILE" | sed -n '1,200p'

# Show the referenced range (around lines 38-44) with context
awk '{print NR "\t" $0}' "$FILE" | sed -n '30,90p'

# Search for existing image loading/error handling in this file
rg -n "onLoad(Start|End)|onError|isImageLoading|hasImageError|SkeletonPlaceholder" "$FILE" || true

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for expo-image Image usage with load/error handlers
rg -n "from 'expo-image'|expo-image" app | head -n 50 || true
rg -n "onLoadStart|onLoadEnd|onError" app -S || true

# Search in entire repo for common expo-image event props
rg -n "onError=" -S . || true
rg -n "onLoad(Start|End)=" -S . || true

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 50391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect a few files that likely use expo-image load/error props
for FILE in \
  "app/containers/ServerItem/index.tsx" \
  "app/containers/Avatar/Avatar.tsx" \
  "app/containers/RoomTypeIcon/OmnichannelRoomIcon.tsx" \
  "app/containers/message/Urls.tsx" \
  "app/containers/message/Components/Attachments/Image/Image.tsx" \
  ; do
  if [ -f "$FILE" ]; then
    echo "===== $FILE ====="
    sed -n '1,220p' "$FILE" | awk '{print NR "\t" $0}' | sed -n '1,220p'
    rg -n "onLoad(Start|End)|onError" "$FILE" || true
  fi
done

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 17844


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "import\s*\{\s*Image\s*\}\s*from\s*'expo-image'" app --glob='*.{ts,tsx}' > /tmp/expo_image_imports.txt || true
# Find onLoadStart/onLoadEnd/onLoad/onError usages in non-snapshot code
rg -n "onLoadStart\s*=|onLoadEnd\s*=|onLoad\s*=|onError\s*=" app --glob='*.{ts,tsx}' > /tmp/image_callbacks.txt || true

# Show a small subset of matches that mention expo-image Image usage patterns
# (filter by nearby presence of "from 'expo-image'" within same file)
python3 - <<'PY'
import re, pathlib

imports=set()
for line in open('/tmp/expo_image_imports.txt','r',encoding='utf-8'):
    # format: path:line:content
    path=line.split(':',1)[0]
    imports.add(path)

matches=[]
for line in open('/tmp/image_callbacks.txt','r',encoding='utf-8'):
    path=line.split(':',1)[0]
    if path in imports:
        matches.append(line.strip())

print("Matches in actual code files that import { Image } from 'expo-image':")
for m in matches[:80]:
    print(m)
print("Total:", len(matches))
PY

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 257


🌐 Web query:

expo-image Image props onLoadStart onLoadEnd onError supported

💡 Result:

The expo-image component supports the following event props for tracking image loading status: onLoadStart: Called when the image starts to load [1][2]. onLoadEnd: Called when the image load completes, whether it succeeds or fails [1][2]. onError: Called when an image fetching error occurs [1][2]. It receives an event object of type ImageErrorEventData [1][2]. These props are supported across the platforms that expo-image covers, which include Android, iOS, tvOS, and Web [3][4]. Note that in addition to the standard component prop, there is also an onError handler used within the useImage hook, which provides the error and a retry function [3][4].

Citations:


Show skeleton while the avatar image is loading and on error
app/views/WorkspaceView/ServerAvatar.tsx renders <Image /> whenever image is truthy, with the skeleton placeholder only shown when image is falsy—so slow/failed remote loads can leave the component blank instead of displaying the skeleton.

Suggested fix
+const ServerAvatar = React.memo(({ url, image }: IServerAvatar): React.JSX.Element => {
+	const [isImageLoading, setIsImageLoading] = React.useState<boolean>(Boolean(image));
+	const [hasImageError, setHasImageError] = React.useState<boolean>(false);
+
+	React.useEffect(() => {
+		setIsImageLoading(Boolean(image));
+		setHasImageError(false);
+	}, [url, image]);
+
 	return (
 		<View style={styles.container}>
-			{image ? (
-				<Image style={[styles.image, { borderColor: colors.strokeLight }]} source={{ uri: `${url}/${image}` }} />
-			) : (
+			{(!image || isImageLoading || hasImageError) ? (
 				<SkeletonPlaceholder backgroundColor={colors.surfaceNeutral} borderRadius={BORDER_RADIUS}>
 					<SkeletonPlaceholder.Item width={SIZE} height={SIZE} />
 				</SkeletonPlaceholder>
+			) : null}
+			{image ? (
+				<Image
+					style={[styles.image, { borderColor: colors.strokeLight }]}
+					source={{ uri: `${url}/${image}` }}
+					onLoadStart={() => setIsImageLoading(true)}
+					onLoadEnd={() => setIsImageLoading(false)}
+					onError={() => {
+						setIsImageLoading(false);
+						setHasImageError(true);
+					}}
+				/>
 			)}
 		</View>
 	);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/WorkspaceView/ServerAvatar.tsx` around lines 38 - 44, ServerAvatar
currently renders Image whenever image is truthy so the SkeletonPlaceholder only
shows when image is falsy; change ServerAvatar to track loading state (e.g.,
isLoading/isError) and render the SkeletonPlaceholder while isLoading or isError
is true, then render the Image when loaded successfully. Add Image onLoad to
clear isLoading and onError to set isError (and stop loading), ensure the Image
source uses `${url}/${image}` only when image exists, and keep existing styles
(styles.image, colors.strokeLight, SIZE, BORDER_RADIUS) so the skeleton displays
during slow or failed network loads.

</View>
);
});
Expand Down