From 1485ca0b374b20ae757b9855fe3d47fdea2a3b0c Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 6 Mar 2026 15:18:57 +0530 Subject: [PATCH 01/40] feat: gallery loading and error state redesign --- package/src/components/Attachment/Gallery.tsx | 61 +++---- .../Attachment/Giphy/GiphyImage.tsx | 11 +- .../ImageLoadingFailedIndicator.tsx | 85 +++++----- .../Attachment/ImageLoadingIndicator.tsx | 31 +--- .../Attachment/ImageReloadIndicator.tsx | 26 --- .../Attachment/__tests__/Gallery.test.js | 6 +- .../Attachment/__tests__/Giphy.test.js | 8 +- package/src/components/Channel/Channel.tsx | 4 - .../Channel/hooks/useCreateMessagesContext.ts | 2 - .../Message/MessageSimple/MessageContent.tsx | 13 +- .../MessageSimple/MessageTextContainer.tsx | 3 +- .../AttachmentUploadProgressIndicator.tsx | 36 +--- .../MessageList/MessageFlashList.tsx | 132 ++++++++------- .../components/MessageList/MessageList.tsx | 156 +++++++++--------- .../UIComponents/Shimmer/ShimmerContext.tsx | 29 ++++ .../UIComponents/Shimmer/ShimmerView.tsx | 78 +++++++++ package/src/components/index.ts | 1 - .../src/components/ui/Badge/RetryBadge.tsx | 76 +++++++++ .../messagesContext/MessagesContext.tsx | 9 +- .../src/contexts/themeContext/utils/theme.ts | 10 +- package/src/hooks/useLoadingImage.tsx | 2 +- 21 files changed, 436 insertions(+), 343 deletions(-) delete mode 100644 package/src/components/Attachment/ImageReloadIndicator.tsx create mode 100644 package/src/components/UIComponents/Shimmer/ShimmerContext.tsx create mode 100644 package/src/components/UIComponents/Shimmer/ShimmerView.tsx create mode 100644 package/src/components/ui/Badge/RetryBadge.tsx diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 41c4731020..f6cd173d63 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -59,7 +59,6 @@ export type GalleryPropsWithContext = Pick & Pick & { @@ -74,7 +73,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { imageGalleryStateStore, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, images, message, onLongPress, @@ -148,8 +146,8 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { images.length !== 1 ? { width: gridWidth, height: gridHeight } : { - height, - width, + minHeight: height, + minWidth: width, }, galleryContainer, ]} @@ -194,7 +192,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { imageGalleryStateStore={imageGalleryStateStore} ImageLoadingFailedIndicator={ImageLoadingFailedIndicator} ImageLoadingIndicator={ImageLoadingIndicator} - ImageReloadIndicator={ImageReloadIndicator} imagesAndVideos={imagesAndVideos} invertedDirections={invertedDirections || false} key={rowIndex} @@ -235,7 +232,6 @@ type GalleryThumbnailProps = { | 'VideoThumbnail' | 'ImageLoadingIndicator' | 'ImageLoadingFailedIndicator' - | 'ImageReloadIndicator' > & Pick & Pick & @@ -248,7 +244,6 @@ const GalleryThumbnail = ({ imageGalleryStateStore, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, imagesAndVideos, invertedDirections, message, @@ -352,7 +347,6 @@ const GalleryThumbnail = ({ borderRadius={imageBorderRadius ?? borderRadius} ImageLoadingFailedIndicator={ImageLoadingFailedIndicator} ImageLoadingIndicator={ImageLoadingIndicator} - ImageReloadIndicator={ImageReloadIndicator} thumbnail={thumbnail} /> )} @@ -379,15 +373,10 @@ const GalleryImageThumbnail = ({ borderRadius, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, thumbnail, }: Pick< GalleryThumbnailProps, - | 'ImageLoadingFailedIndicator' - | 'ImageLoadingIndicator' - | 'ImageReloadIndicator' - | 'thumbnail' - | 'borderRadius' + 'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius' >) => { const { isLoadingImage, @@ -406,15 +395,9 @@ const GalleryImageThumbnail = ({ const styles = useStyles(); return ( - + {isLoadingImageError ? ( - <> - - - + ) : ( <> setTimeout(() => setLoadingImage(false), 0)} onLoadStart={() => setLoadingImage(true)} resizeMode={thumbnail.resizeMode} - style={[borderRadius, gallery.image]} + style={gallery.image} uri={thumbnail.url} /> - {isLoadingImage && ( - - - - )} + {isLoadingImage ? : null} )} @@ -512,7 +491,6 @@ export const Gallery = (props: GalleryProps) => { additionalPressableProps: propAdditionalPressableProps, ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator, ImageLoadingIndicator: PropImageLoadingIndicator, - ImageReloadIndicator: PropImageReloadIndicator, images: propImages, message: propMessage, myMessageTheme: propMyMessageTheme, @@ -542,7 +520,6 @@ export const Gallery = (props: GalleryProps) => { additionalPressableProps: contextAdditionalPressableProps, ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator, ImageLoadingIndicator: ContextImageLoadingIndicator, - ImageReloadIndicator: ContextImageReloadIndicator, myMessageTheme: contextMyMessageTheme, VideoThumbnail: ContextVideoThumnbnail, } = useMessagesContext(); @@ -567,7 +544,6 @@ export const Gallery = (props: GalleryProps) => { const ImageLoadingFailedIndicator = PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator; const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator; - const ImageReloadIndicator = PropImageReloadIndicator || ContextImageReloadIndicator; const myMessageTheme = propMyMessageTheme || contextMyMessageTheme; const messageContentOrder = propMessageContentOrder || contextMessageContentOrder; @@ -585,7 +561,6 @@ export const Gallery = (props: GalleryProps) => { imageGalleryStateStore, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, images, message, myMessageTheme, @@ -607,6 +582,7 @@ const useStyles = () => { const { theme: { semantics }, } = useTheme(); + const { isMyMessage } = useMessageContext(); return useMemo(() => { return StyleSheet.create({ errorTextSize: { @@ -626,11 +602,15 @@ const useStyles = () => { imageContainer: {}, image: { flex: 1, + backgroundColor: isMyMessage + ? semantics.chatBgAttachmentOutgoing + : semantics.chatBgAttachmentIncoming, + overflow: 'hidden', }, imageLoadingErrorIndicatorStyle: { - bottom: 4, - left: 4, - position: 'absolute', + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', }, imageLoadingIndicatorContainer: { height: '100%', @@ -658,8 +638,17 @@ const useStyles = () => { lineHeight: primitives.typographyLineHeightRelaxed, fontWeight: primitives.typographyFontWeightSemiBold, }, + imageLoadingErrorContainer: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + }, + imageLoadingErrorWrapper: { + ...StyleSheet.absoluteFillObject, + overflow: 'hidden', + }, }); - }, [semantics]); + }, [semantics, isMyMessage]); }; Gallery.displayName = 'Gallery{messageSimple{gallery}}'; diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx index d3f4dc551a..f9c016283f 100644 --- a/package/src/components/Attachment/Giphy/GiphyImage.tsx +++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx @@ -39,8 +39,13 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => { const { giphy: giphyData, image_url, thumb_url, type } = attachment; - const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } = - useLoadingImage(); + const { + isLoadingImage, + isLoadingImageError, + setLoadingImage, + setLoadingImageError, + onReloadImage, + } = useLoadingImage(); const { theme: { @@ -93,7 +98,7 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => { /> {isLoadingImageError && ( - + )} {isLoadingImage && ( diff --git a/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx b/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx index 2f135835e8..aef29c352d 100644 --- a/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx @@ -1,56 +1,45 @@ -import React from 'react'; -import { StyleSheet, Text, View, ViewProps } from 'react-native'; +import React, { useMemo } from 'react'; +import { Pressable, StyleSheet, ViewProps } from 'react-native'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; - -import { Warning } from '../../icons/Warning'; - -const WARNING_ICON_SIZE = 14; +import { RetryBadge } from '../ui/Badge/RetryBadge'; + +export type ImageLoadingFailedIndicatorProps = ViewProps & { + /** + * Callback to reload the image + * @returns Callback to reload the image + */ + onReloadImage: () => void; +}; -const styles = StyleSheet.create({ - container: { - alignContent: 'center', - alignItems: 'center', - borderRadius: 20, - flexDirection: 'row', - justifyContent: 'center', - }, - errorText: { - fontSize: 8, - justifyContent: 'center', - paddingHorizontal: 8, - }, - warningIconStyle: { - borderRadius: 24, - marginLeft: 4, - marginTop: 6, - }, -}); +export const ImageLoadingFailedIndicator = ({ + onReloadImage, +}: ImageLoadingFailedIndicatorProps) => { + const styles = useStyles(); -export type ImageLoadingFailedIndicatorProps = ViewProps; + return ( + + + + ); +}; -export const ImageLoadingFailedIndicator = (props: ImageLoadingFailedIndicatorProps) => { +const useStyles = () => { const { - theme: { - colors: { accent_red, overlay, white }, - }, + theme: { semantics }, } = useTheme(); - - const { t } = useTranslationContext(); - - const { style, ...rest } = props; - return ( - - - - {t('Error loading')} - - - ); + return useMemo(() => { + return StyleSheet.create({ + imageLoadingErrorIndicatorStyle: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: semantics.backgroundCoreOverlayLight, + }, + }); + }, [semantics]); }; diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index 019aa420d4..b42200c9af 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -1,31 +1,14 @@ import React from 'react'; -import { ActivityIndicator, StyleSheet, View, ViewProps } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { ShimmerView } from '../UIComponents/Shimmer/ShimmerView'; -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - display: 'flex', - justifyContent: 'center', - width: '100%', - }, -}); - -export type ImageLoadingIndicatorProps = ViewProps; - -export const ImageLoadingIndicator = (props: ImageLoadingIndicatorProps) => { - const { - theme: { - messageSimple: { - loadingIndicator: { container }, - }, - }, - } = useTheme(); - const { style, ...rest } = props; +export const ImageLoadingIndicator = () => { return ( - - + + + + ); }; diff --git a/package/src/components/Attachment/ImageReloadIndicator.tsx b/package/src/components/Attachment/ImageReloadIndicator.tsx deleted file mode 100644 index 9f3b2e24a0..0000000000 --- a/package/src/components/Attachment/ImageReloadIndicator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { Pressable } from 'react-native'; - -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { Refresh } from '../../icons'; - -const REFRESH_ICON_SIZE = 24; - -export type ImageReloadIndicatorProps = { - onReloadImage: () => void; - style: React.ComponentProps['style']; -}; - -export const ImageReloadIndicator = ({ onReloadImage, style }: ImageReloadIndicatorProps) => { - const { - theme: { - colors: { grey_dark }, - }, - } = useTheme(); - - return ( - - - - ); -}; diff --git a/package/src/components/Attachment/__tests__/Gallery.test.js b/package/src/components/Attachment/__tests__/Gallery.test.js index 57a06a0f3c..1301834c85 100644 --- a/package/src/components/Attachment/__tests__/Gallery.test.js +++ b/package/src/components/Attachment/__tests__/Gallery.test.js @@ -272,7 +272,7 @@ describe('Gallery', () => { fireEvent(screen.getByLabelText('Gallery Image'), 'error', { nativeEvent: { error: 'error loading image' }, }); - expect(screen.getByAccessibilityHint('image-loading-error')).toBeTruthy(); + expect(screen.getByLabelText('Image Loading Error Indicator')).toBeTruthy(); }); it('should render a loading indicator and when successful render the image', async () => { @@ -289,10 +289,10 @@ describe('Gallery', () => { }); fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadStart'); - expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy(); + expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadFinish'); - waitForElementToBeRemoved(() => screen.getByAccessibilityHint('image-loading')); + waitForElementToBeRemoved(() => screen.getByLabelText('Image Loading Indicator')); expect(screen.getByLabelText('Gallery Image')).toBeTruthy(); }); }); diff --git a/package/src/components/Attachment/__tests__/Giphy.test.js b/package/src/components/Attachment/__tests__/Giphy.test.js index 2aed7e6385..0cc42b819a 100644 --- a/package/src/components/Attachment/__tests__/Giphy.test.js +++ b/package/src/components/Attachment/__tests__/Giphy.test.js @@ -321,7 +321,7 @@ describe('Giphy', () => { }); await waitFor(() => { - expect(screen.getByAccessibilityHint('image-loading-error')).toBeTruthy(); + expect(screen.getByLabelText('Image Loading Error Indicator')).toBeTruthy(); }); }); @@ -336,7 +336,7 @@ describe('Giphy', () => { , ); await waitFor(() => { - expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy(); + expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); }); act(() => { @@ -344,14 +344,14 @@ describe('Giphy', () => { }); await waitFor(() => { - expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy(); + expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); }); act(() => { fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'onLoad'); }); - waitForElementToBeRemoved(() => screen.getByAccessibilityHint('image-loading')); + waitForElementToBeRemoved(() => screen.getByLabelText('Image Loading Indicator')); await waitFor(() => { expect(screen.getByLabelText('Giphy Attachment Image')).toBeTruthy(); diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 45091a78aa..03a2c3a0b9 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -119,7 +119,6 @@ import { Gallery as GalleryDefault } from '../Attachment/Gallery'; import { Giphy as GiphyDefault } from '../Attachment/Giphy'; import { ImageLoadingFailedIndicator as ImageLoadingFailedIndicatorDefault } from '../Attachment/ImageLoadingFailedIndicator'; import { ImageLoadingIndicator as ImageLoadingIndicatorDefault } from '../Attachment/ImageLoadingIndicator'; -import { ImageReloadIndicator as ImageReloadIndicatorDefault } from '../Attachment/ImageReloadIndicator'; import { URLPreview as URLPreviewDefault } from '../Attachment/UrlPreview'; import { VideoThumbnail as VideoThumbnailDefault } from '../Attachment/VideoThumbnail'; import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker'; @@ -351,7 +350,6 @@ export type ChannelPropsWithContext = Pick & | 'isAttachmentEqual' | 'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' - | 'ImageReloadIndicator' | 'markdownRules' | 'Message' | 'MessageActionList' @@ -647,7 +645,6 @@ const ChannelWithContext = (props: PropsWithChildren) = ImageAttachmentUploadPreview = ImageAttachmentUploadPreviewDefault, ImageLoadingFailedIndicator = ImageLoadingFailedIndicatorDefault, ImageLoadingIndicator = ImageLoadingIndicatorDefault, - ImageReloadIndicator = ImageReloadIndicatorDefault, initialScrollToFirstUnreadMessage = false, InlineDateSeparator = InlineDateSeparatorDefault, InlineUnreadIndicator = InlineUnreadIndicatorDefault, @@ -1932,7 +1929,6 @@ const ChannelWithContext = (props: PropsWithChildren) = hasCreatePoll === undefined ? pollCreationEnabled : hasCreatePoll && pollCreationEnabled, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, initialScrollToFirstUnreadMessage: !messageId && initialScrollToFirstUnreadMessage, // when messageId is set, we scroll to the messageId instead of first unread InlineDateSeparator, InlineUnreadIndicator, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 3db83d4d65..178ce6a077 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -42,7 +42,6 @@ export const useCreateMessagesContext = ({ hasCreatePoll, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, initialScrollToFirstUnreadMessage, InlineDateSeparator, InlineUnreadIndicator, @@ -159,7 +158,6 @@ export const useCreateMessagesContext = ({ hasCreatePoll, ImageLoadingFailedIndicator, ImageLoadingIndicator, - ImageReloadIndicator, initialScrollToFirstUnreadMessage, InlineDateSeparator, InlineUnreadIndicator, diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx index caf378e4cf..cbd6fa1569 100644 --- a/package/src/components/Message/MessageSimple/MessageContent.tsx +++ b/package/src/components/Message/MessageSimple/MessageContent.tsx @@ -169,7 +169,6 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { messageGroupedSingleOrBottomContainer, messageGroupedTopContainer, replyContainer, - textWrapper, wrapper, }, }, @@ -363,11 +362,9 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { } })} - - {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : ( - - )} - + {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : ( + + )} @@ -665,7 +662,5 @@ const styles = StyleSheet.create({ rightAlignItems: { alignItems: 'flex-end', }, - textWrapper: { - paddingHorizontal: primitives.spacingSm, - }, + textWrapper: {}, }); diff --git a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx index e3af2215f1..9af8b5747d 100644 --- a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx +++ b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx @@ -17,9 +17,10 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import type { MarkdownStyle, Theme } from '../../../contexts/themeContext/utils/theme'; import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage'; +import { primitives } from '../../../theme'; const styles = StyleSheet.create({ - textContainer: { maxWidth: 256 }, + textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingSm }, }); export type MessageTextProps = MessageTextContainerProps & { diff --git a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx index 919ee1c1aa..7ea9b848fe 100644 --- a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx +++ b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx @@ -5,9 +5,9 @@ import { LocalAttachmentUploadMetadata } from 'stream-chat'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; import { ExclamationCircle } from '../../../../icons/ExclamationCircle'; -import { RotateCircle } from '../../../../icons/RotateCircle'; import { Warning } from '../../../../icons/Warning'; import { primitives } from '../../../../theme'; +import { RetryBadge } from '../../../ui/Badge/RetryBadge'; export const FileUploadInProgressIndicator = () => { const { @@ -128,20 +128,9 @@ export type ImageUploadRetryIndicatorProps = { }; export const ImageUploadRetryIndicator = ({ onRetryHandler }: ImageUploadRetryIndicatorProps) => { - const styles = useImageUploadRetryIndicatorStyles(); - const { - theme: { - semantics, - messageInput: { imageUploadRetryIndicator }, - }, - } = useTheme(); return ( - - + + ); }; @@ -174,25 +163,6 @@ const useImageUploadInProgressIndicatorStyles = () => { }); }; -const useImageUploadRetryIndicatorStyles = () => { - const { - theme: { semantics }, - } = useTheme(); - return StyleSheet.create({ - container: { - backgroundColor: semantics.accentError, - alignItems: 'center', - justifyContent: 'center', - borderRadius: primitives.radiusMax, - borderWidth: 2, - borderColor: semantics.textOnAccent, - alignSelf: 'center', - width: 32, - height: 32, - }, - }); -}; - const useImageUploadNotSupportedIndicatorStyles = () => { const { theme: { semantics }, diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 0de8b3e356..4e2bb64ee2 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -8,7 +8,7 @@ import { ViewToken, } from 'react-native'; -import Animated, { LinearTransition } from 'react-native-reanimated'; +import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated'; import { FlashListProps, FlashListRef, useFlashListContext } from '@shopify/flash-list'; import type { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat'; @@ -56,6 +56,7 @@ import { useStableCallback, useStateStore } from '../../hooks'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; +import { ShimmerProvider } from '../UIComponents/Shimmer/ShimmerContext'; let FlashList; @@ -700,6 +701,8 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } }); + const visibleMessages = useSharedValue([]); + /** * FlatList doesn't accept changeable function for onViewableItemsChanged prop. * Thus useRef. @@ -709,6 +712,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => }: { viewableItems: ViewToken[] | undefined; }) => { + visibleMessages.value = viewableItems?.map((viewToken) => viewToken.item.message.id) ?? []; if (!viewableItems) { return; } @@ -1041,72 +1045,74 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } return ( - - {processedMessageList.length === 0 && !thread ? ( - - {EmptyStateIndicator ? : null} + + + {processedMessageList.length === 0 && !thread ? ( + + {EmptyStateIndicator ? : null} + + ) : ( + + + + )} + + {messageListLengthAfterUpdate && StickyHeader ? ( + + ) : null} - ) : ( - - + - - )} - - {messageListLengthAfterUpdate && StickyHeader ? ( - + + + {isUnreadNotificationOpen && !threadList ? ( + + + ) : null} - - - - - {isUnreadNotificationOpen && !threadList ? ( - - - - ) : null} - + ); }; diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 4766bc8564..09a351ef28 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -11,7 +11,7 @@ import { ViewToken, } from 'react-native'; -import Animated, { LinearTransition } from 'react-native-reanimated'; +import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated'; import debounce from 'lodash/debounce'; @@ -69,6 +69,7 @@ import { bumpOverlayLayoutRevision } from '../../state-store'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; +import { ShimmerProvider } from '../UIComponents/Shimmer/ShimmerContext'; // This is just to make sure that the scrolling happens in a different task queue. // TODO: Think if we really need this and strive to remove it if we can. @@ -579,6 +580,8 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { } }); + const visibleMessages = useSharedValue([]); + /** * FlatList doesn't accept changeable function for onViewableItemsChanged prop. * Thus useRef. @@ -588,6 +591,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { }: { viewableItems: ViewToken[] | undefined; }) => { + visibleMessages.value = viewableItems?.map((viewToken) => viewToken.item.message.id) ?? []; viewabilityChangedCallback({ inverted, viewableItems }); if (!viewableItems) { @@ -1253,87 +1257,89 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { // TODO: Make sure this is actually overridable as the previous FlatList was. return ( - - {/* Don't show the empty list indicator for Thread messages */} - {processedMessageList.length === 0 && !thread ? ( - - {EmptyStateIndicator ? : null} - - ) : ( - - + + {/* Don't show the empty list indicator for Thread messages */} + {processedMessageList.length === 0 && !thread ? ( + + {EmptyStateIndicator ? : null} + + ) : ( + + - - )} - - {messageListLengthAfterUpdate && StickyHeader ? ( - + maintainVisibleContentPosition={maintainVisibleContentPosition} + maxToRenderPerBatch={30} + onContentSizeChange={onContentSizeChange} + onLayout={onLayout} + onMomentumScrollEnd={onUserScrollEvent} + onScroll={handleScroll} + onScrollBeginDrag={onScrollBeginDrag} + onScrollEndDrag={onScrollEndDrag} + onScrollToIndexFailed={onScrollToIndexFailedRef.current} + onTouchEnd={dismissImagePicker} + onViewableItemsChanged={stableOnViewableItemsChanged} + ref={refCallback} + renderItem={renderItem} + scrollEventThrottle={isLiveStreaming ? 16 : undefined} + showsVerticalScrollIndicator={false} + // @ts-expect-error Safe to do for now + strictMode={isLiveStreaming} + style={flatListStyle} + testID='message-flat-list' + viewabilityConfig={flatListViewabilityConfig} + {...additionalFlatListPropsExcludingStyle} + /> + + )} + + {messageListLengthAfterUpdate && StickyHeader ? ( + + ) : null} + + {scrollToBottomButtonVisible ? ( + + + + ) : null} + + + {isUnreadNotificationOpen && !threadList ? ( + + + ) : null} - {scrollToBottomButtonVisible ? ( - - - - ) : null} - - - {isUnreadNotificationOpen && !threadList ? ( - - - - ) : null} - + ); }; diff --git a/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx b/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx new file mode 100644 index 0000000000..ff1d9d8022 --- /dev/null +++ b/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'; +import { SharedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; + +type ShimmerContextValue = { + progress: SharedValue; + visibleMessages: SharedValue; +}; + +const ShimmerContext = createContext({} as ShimmerContextValue); + +const SHIMMER_WIDTH = 150; + +type ShimmerProviderProps = PropsWithChildren<{ + visibleMessages: SharedValue; +}>; + +export const ShimmerProvider = ({ children, visibleMessages }: ShimmerProviderProps) => { + const progress = useSharedValue(-SHIMMER_WIDTH); + + useEffect(() => { + progress.value = withRepeat(withTiming(SHIMMER_WIDTH, { duration: 1200 }), -1, true); + }, [progress]); + + const contextValue = useMemo(() => ({ progress, visibleMessages }), [progress, visibleMessages]); + + return {children}; +}; + +export const useShimmerContext = () => useContext(ShimmerContext); diff --git a/package/src/components/UIComponents/Shimmer/ShimmerView.tsx b/package/src/components/UIComponents/Shimmer/ShimmerView.tsx new file mode 100644 index 0000000000..b1403932df --- /dev/null +++ b/package/src/components/UIComponents/Shimmer/ShimmerView.tsx @@ -0,0 +1,78 @@ +import React, { PropsWithChildren } from 'react'; +import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import Animated, { useAnimatedStyle } from 'react-native-reanimated'; +import Svg, { Rect, Defs, LinearGradient, Stop } from 'react-native-svg'; + +import { useShimmerContext } from './ShimmerContext'; + +import { useMessageContext } from '../../../contexts/messageContext/MessageContext'; +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; + +type Props = PropsWithChildren<{ + style?: StyleProp; +}>; + +export const ShimmerView = ({ children, style }: Props) => { + const { progress, visibleMessages } = useShimmerContext(); + const { message } = useMessageContext(); + + const messageId = message?.id; + + const { + theme: { + shimmer: { width, height }, + }, + } = useTheme(); + const styles = useStyles(); + + const animatedStyle = useAnimatedStyle(() => { + return visibleMessages.value.includes(messageId) + ? { + transform: [{ translateX: progress?.value ?? 0 }], + } + : {}; + }, [messageId]); + + return ( + + + + + + + + + + + + + + + + {children} + + ); +}; + +const useStyles = () => { + const { + theme: { + shimmer: { width, height }, + }, + } = useTheme(); + return StyleSheet.create({ + container: { + flex: 1, + overflow: 'hidden', + }, + shimmerContainer: { + width, + height, + }, + content: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + }, + }); +}; diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 9c0c493935..b5402f95a1 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -7,7 +7,6 @@ export * from './Attachment/FileIcon'; export * from './Attachment/Gallery'; export * from './Attachment/Giphy'; export * from './Attachment/VideoThumbnail'; -export * from './Attachment/ImageReloadIndicator'; export * from './Attachment/UrlPreview'; export * from './Attachment/utils/buildGallery/buildGallery'; diff --git a/package/src/components/ui/Badge/RetryBadge.tsx b/package/src/components/ui/Badge/RetryBadge.tsx new file mode 100644 index 0000000000..2b6876bca3 --- /dev/null +++ b/package/src/components/ui/Badge/RetryBadge.tsx @@ -0,0 +1,76 @@ +import React, { useMemo } from 'react'; +import { StyleProp, StyleSheet, View, ViewProps, ViewStyle } from 'react-native'; + +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; +import { RotateCircle } from '../../../icons/RotateCircle'; +import { primitives } from '../../../theme'; + +const sizes = { + lg: { + height: 32, + width: 32, + }, + md: { + height: 24, + width: 24, + }, +}; + +const iconSizes = { + lg: { + height: 16, + width: 16, + }, + md: { + height: 12, + width: 12, + }, +}; + +export type RetryBadgeProps = ViewProps & { + /** + * The size of the badge + * @default 'md' + * @type {'lg' | 'md'} + */ + size: 'lg' | 'md'; + /** + * The style of the badge + * @default undefined + * @type {StyleProp} + */ + style?: StyleProp; +}; + +export const RetryBadge = (props: RetryBadgeProps) => { + const { size = 'md', style, ...rest } = props; + const { + theme: { semantics }, + } = useTheme(); + const styles = useStyles(); + return ( + + + + ); +}; + +const useStyles = () => { + const { + theme: { semantics }, + } = useTheme(); + return useMemo(() => { + return StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + borderRadius: primitives.radiusMax, + backgroundColor: semantics.badgeBgError, + }, + }); + }, [semantics]); +}; diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 52910b4088..53adab5444 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -25,8 +25,6 @@ import type { FileIconProps } from '../../components/Attachment/FileIcon'; import type { GalleryProps } from '../../components/Attachment/Gallery'; import type { GiphyProps } from '../../components/Attachment/Giphy'; import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachment/ImageLoadingFailedIndicator'; -import type { ImageLoadingIndicatorProps } from '../../components/Attachment/ImageLoadingIndicator'; -import { ImageReloadIndicatorProps } from '../../components/Attachment/ImageReloadIndicator'; import type { URLPreviewProps } from '../../components/Attachment/UrlPreview'; import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail'; import type { @@ -162,15 +160,10 @@ export type MessagesContextValue = Pick; - /** - * The indicator rendered at the center of an image whenever its loading fails, used to trigger retries. - */ - ImageReloadIndicator: React.ComponentType; - /** * The indicator rendered when image is loading. By default renders */ - ImageLoadingIndicator: React.ComponentType; + ImageLoadingIndicator: React.ComponentType; /** * When true, messageList will be scrolled at first unread message, when opened. diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index a5b2e9a131..df4e447f31 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -594,7 +594,6 @@ export type Theme = { container: ViewStyle; containerInner: ViewStyle; contentContainer: ViewStyle; - textWrapper: ViewStyle; editedTimestampContainer: ViewStyle; errorContainer: ViewStyle; errorIcon: IconProps; @@ -992,6 +991,10 @@ export type Theme = { waveform: ViewStyle; }; semantics: typeof lightSemantics; // themed semantics have the same type + shimmer: { + width: number; + height: number; + }; }; export const defaultTheme: Theme = { @@ -1482,7 +1485,6 @@ export const defaultTheme: Theme = { container: {}, containerInner: {}, contentContainer: {}, - textWrapper: {}, editedTimestampContainer: {}, errorContainer: { paddingRight: 12, @@ -1871,4 +1873,8 @@ export const defaultTheme: Theme = { thumb: {}, waveform: {}, }, + shimmer: { + width: 300, + height: 192, + }, }; diff --git a/package/src/hooks/useLoadingImage.tsx b/package/src/hooks/useLoadingImage.tsx index 98b8bdda75..9c000f4341 100644 --- a/package/src/hooks/useLoadingImage.tsx +++ b/package/src/hooks/useLoadingImage.tsx @@ -30,7 +30,7 @@ function reducer(prevState: ImageState, action: Action) { } export const useLoadingImage = () => { const [imageState, dispatch] = useReducer(reducer, { - isLoadingImage: true, + isLoadingImage: false, isLoadingImageError: false, }); const { isLoadingImage, isLoadingImageError } = imageState; From 48240e3e5b592692d191cc3d79e4726af1538d94 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 11:52:58 +0100 Subject: [PATCH 02/40] perf: optimize images loading state --- package/src/components/Attachment/Gallery.tsx | 58 ++++++++++++++++--- package/src/hooks/useLoadingImage.tsx | 9 +++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index f6cd173d63..aa99cf3ed5 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -1,5 +1,12 @@ -import React, { useMemo } from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { + ImageErrorEventData, + NativeSyntheticEvent, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; import type { Attachment, LocalMessage } from 'stream-chat'; @@ -35,6 +42,7 @@ import { import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useLoadingImage } from '../../hooks/useLoadingImage'; +import { useStableCallback } from '../../hooks/useStableCallback'; import { isVideoPlayerAvailable } from '../../native'; import { primitives } from '../../theme'; import { FileTypes } from '../../types/types'; @@ -66,6 +74,8 @@ export type GalleryPropsWithContext = Pick { const { additionalPressableProps, @@ -385,6 +395,7 @@ const GalleryImageThumbnail = ({ setLoadingImage, setLoadingImageError, } = useLoadingImage(); + const loadingIndicatorTimerRef = useRef | null>(null); const { theme: { @@ -394,6 +405,39 @@ const GalleryImageThumbnail = ({ const styles = useStyles(); + const clearLoadingIndicatorTimer = useStableCallback(() => { + if (loadingIndicatorTimerRef.current) { + clearTimeout(loadingIndicatorTimerRef.current); + loadingIndicatorTimerRef.current = null; + } + }); + + const onLoadStart = useStableCallback(() => { + clearLoadingIndicatorTimer(); + setLoadingImageError(false); + loadingIndicatorTimerRef.current = setTimeout(() => { + setLoadingImage(true); + loadingIndicatorTimerRef.current = null; + }, IMAGE_LOADING_INDICATOR_DELAY_MS); + }); + + const onLoadEnd = useStableCallback(() => { + clearLoadingIndicatorTimer(); + setLoadingImage(false); + setLoadingImageError(false); + }); + + const onError = useStableCallback( + ({ nativeEvent: { error } }: NativeSyntheticEvent) => { + clearLoadingIndicatorTimer(); + console.warn(error); + setLoadingImage(false); + setLoadingImageError(true); + }, + ); + + useEffect(() => clearLoadingIndicatorTimer, [clearLoadingIndicatorTimer]); + return ( {isLoadingImageError ? ( @@ -401,13 +445,9 @@ const GalleryImageThumbnail = ({ ) : ( <> { - console.warn(error); - setLoadingImage(false); - setLoadingImageError(true); - }} - onLoadEnd={() => setTimeout(() => setLoadingImage(false), 0)} - onLoadStart={() => setLoadingImage(true)} + onError={onError} + onLoadEnd={onLoadEnd} + onLoadStart={onLoadStart} resizeMode={thumbnail.resizeMode} style={gallery.image} uri={thumbnail.url} diff --git a/package/src/hooks/useLoadingImage.tsx b/package/src/hooks/useLoadingImage.tsx index 9c000f4341..bb7d44022e 100644 --- a/package/src/hooks/useLoadingImage.tsx +++ b/package/src/hooks/useLoadingImage.tsx @@ -15,14 +15,23 @@ type Action = function reducer(prevState: ImageState, action: Action) { switch (action.type) { case 'reloadImage': + if (prevState.isLoadingImage && !prevState.isLoadingImageError) { + return prevState; + } return { ...prevState, isLoadingImage: true, isLoadingImageError: false, }; case 'setLoadingImage': + if (prevState.isLoadingImage === action.isLoadingImage) { + return prevState; + } return { ...prevState, isLoadingImage: action.isLoadingImage }; case 'setLoadingImageError': + if (prevState.isLoadingImageError === action.isLoadingImageError) { + return prevState; + } return { ...prevState, isLoadingImageError: action.isLoadingImageError }; default: return prevState; From 8b9f7eeb119ffe807275ea0724c93bc3ef0755fd Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 14:07:45 +0100 Subject: [PATCH 03/40] feat: initial pass at implementing gradient shimmer on android --- package/native-package/android/build.gradle | 7 +- .../StreamChatReactNativePackage.java | 8 + .../StreamShimmerFrameLayout.kt | 213 ++++++++++++++++++ .../StreamShimmerViewManager.kt | 82 +++++++ package/native-package/package.json | 2 +- .../StreamShimmerViewNativeComponent.ts | 17 ++ package/src/components/Attachment/Gallery.tsx | 20 +- .../Attachment/ImageLoadingIndicator.tsx | 37 ++- .../NativeShimmerView.android.tsx | 22 ++ .../UIComponents/NativeShimmerView.tsx | 16 ++ 10 files changed, 411 insertions(+), 13 deletions(-) create mode 100644 package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt create mode 100644 package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt create mode 100644 package/native-package/src/native/StreamShimmerViewNativeComponent.ts create mode 100644 package/src/components/UIComponents/NativeShimmerView.android.tsx create mode 100644 package/src/components/UIComponents/NativeShimmerView.tsx diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 1c0d25b343..95bdeb9dc7 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.2.1" - + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${project.properties['ImageResizer_kotlinVersion']}" } } @@ -15,6 +15,7 @@ def isNewArchitectureEnabled() { } apply plugin: "com.android.library" +apply plugin: "kotlin-android" def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } @@ -65,6 +66,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = "17" + } + sourceSets { main { if (isNewArchitectureEnabled()) { diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java index 203b72b857..742e03f4ba 100644 --- a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java @@ -6,9 +6,12 @@ import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.TurboReactPackage; +import com.facebook.react.uimanager.ViewManager; import java.util.HashMap; +import java.util.Collections; +import java.util.List; import java.util.Map; public class StreamChatReactNativePackage extends TurboReactPackage { @@ -42,4 +45,9 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { return moduleInfos; }; } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.singletonList(new StreamShimmerViewManager()); + } } diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt new file mode 100644 index 0000000000..b0cbdcd6ef --- /dev/null +++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -0,0 +1,213 @@ +package com.streamchatreactnative + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Shader +import android.util.AttributeSet +import android.view.animation.LinearInterpolator +import android.widget.FrameLayout +import kotlin.math.roundToInt + +class StreamShimmerFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : FrameLayout(context, attrs) { + private var baseColor: Int = DEFAULT_BASE_COLOR + private var highlightColor: Int = DEFAULT_HIGHLIGHT_COLOR + private var gradientColor: Int = DEFAULT_GRADIENT_COLOR + private var gradientWidth: Float = 0f + private var gradientHeight: Float = 0f + private var enabled: Boolean = true + + private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val shimmerMatrix = Matrix() + + private var shimmerShader: LinearGradient? = null + private var shimmerTranslateX: Float = 0f + private var animator: ValueAnimator? = null + + init { + setWillNotDraw(false) + } + + fun setBaseColor(color: Int) { + if (baseColor == color) return + baseColor = color + rebuildShimmerShader() + invalidate() + } + + fun setHighlightColor(color: Int) { + if (highlightColor == color) return + highlightColor = color + rebuildShimmerShader() + invalidate() + } + + fun setGradientColor(color: Int) { + if (gradientColor == color) return + gradientColor = color + invalidate() + } + + fun setGradientWidth(widthPx: Float) { + if (gradientWidth == widthPx) return + gradientWidth = widthPx + invalidate() + } + + fun setGradientHeight(heightPx: Float) { + if (gradientHeight == heightPx) return + gradientHeight = heightPx + invalidate() + } + + fun setShimmerEnabled(enabled: Boolean) { + if (this.enabled == enabled) return + this.enabled = enabled + updateAnimatorState() + invalidate() + } + + fun updateAnimatorState() { + if (enabled && isAttachedToWindow && width > 0) { + startShimmer() + } else { + stopShimmer() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateAnimatorState() + } + + override fun onDetachedFromWindow() { + stopShimmer() + super.onDetachedFromWindow() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + rebuildShimmerShader() + updateAnimatorState() + } + + override fun dispatchDraw(canvas: Canvas) { + val viewWidth = width.toFloat() + val viewHeight = height.toFloat() + if (viewWidth <= 0f || viewHeight <= 0f) { + super.dispatchDraw(canvas) + return + } + + basePaint.color = baseColor + canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint) + + drawShimmer(canvas, viewWidth, viewHeight) + drawGradient(canvas, viewWidth, viewHeight) + + super.dispatchDraw(canvas) + } + + private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) { + if (!enabled) return + + val shader = shimmerShader ?: return + + shimmerMatrix.setTranslate(shimmerTranslateX, 0f) + shader.setLocalMatrix(shimmerMatrix) + shimmerPaint.shader = shader + canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint) + shimmerPaint.shader = null + } + + private fun drawGradient(canvas: Canvas, viewWidth: Float, viewHeight: Float) { + if (gradientWidth <= 0f || gradientHeight <= 0f) return + + val left = (viewWidth - gradientWidth) / 2f + val top = (viewHeight - gradientHeight) / 2f + val right = left + gradientWidth + val bottom = top + gradientHeight + val gradient = LinearGradient( + left, + top, + right, + top, + intArrayOf( + colorWithAlpha(gradientColor, 0f), + colorWithAlpha(gradientColor, GRADIENT_CENTER_ALPHA), + colorWithAlpha(gradientColor, 0f), + ), + floatArrayOf(0f, 0.5f, 1f), + Shader.TileMode.CLAMP, + ) + gradientPaint.shader = gradient + canvas.drawRect(left, top, right, bottom, gradientPaint) + gradientPaint.shader = null + } + + private fun rebuildShimmerShader() { + val viewWidth = width.toFloat() + if (viewWidth <= 0f) { + shimmerShader = null + return + } + + val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) + shimmerShader = LinearGradient( + 0f, + 0f, + shimmerWidth, + 0f, + intArrayOf(baseColor, highlightColor, baseColor), + floatArrayOf(0f, 0.5f, 1f), + Shader.TileMode.CLAMP, + ) + } + + private fun startShimmer() { + if (animator != null) return + + val viewWidth = width.toFloat() + if (viewWidth <= 0f) return + + val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) + animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply { + duration = SHIMMER_DURATION_MS + repeatCount = ValueAnimator.INFINITE + interpolator = LinearInterpolator() + addUpdateListener { + shimmerTranslateX = it.animatedValue as Float + invalidate() + } + start() + } + } + + private fun stopShimmer() { + animator?.cancel() + animator = null + } + + private fun colorWithAlpha(color: Int, alphaFactor: Float): Int { + val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255) + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) + } + + companion object { + private const val DEFAULT_BASE_COLOR = 0x00FFFFFF + private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF + private const val DEFAULT_GRADIENT_COLOR = Color.WHITE + private const val SHIMMER_DURATION_MS = 1200L + private const val SHIMMER_STRIP_WIDTH_RATIO = 0.35f + private const val GRADIENT_CENTER_ALPHA = 0.35f + } +} diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt new file mode 100644 index 0000000000..73022d4eb1 --- /dev/null +++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt @@ -0,0 +1,82 @@ +package com.streamchatreactnative + +import androidx.annotation.NonNull +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate +import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface + +class StreamShimmerViewManager : ViewGroupManager(), + StreamShimmerViewManagerInterface { + private val delegate = StreamShimmerViewManagerDelegate(this) + + override fun getName(): String = REACT_CLASS + + @NonNull + override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout { + val layout = StreamShimmerFrameLayout(reactContext) + layout.updateAnimatorState() + return layout + } + + override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) { + super.onAfterUpdateTransaction(view) + view.updateAnimatorState() + } + + override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) { + parent.addView(child, index) + } + + override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View { + return parent.getChildAt(index) + } + + override fun getChildCount(parent: StreamShimmerFrameLayout): Int { + return parent.childCount + } + + override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) { + parent.removeViewAt(index) + } + + override fun getDelegate(): ViewManagerDelegate = delegate + + override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) { + view.setShimmerEnabled(enabled) + } + + override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) { + view.setBaseColor(color ?: DEFAULT_BASE_COLOR) + } + + override fun setHighlightColor(view: StreamShimmerFrameLayout, color: Int?) { + view.setHighlightColor(color ?: DEFAULT_HIGHLIGHT_COLOR) + } + + override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) { + view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR) + } + + override fun setGradientWidth(view: StreamShimmerFrameLayout, widthDp: Double) { + view.setGradientWidth(PixelUtil.toPixelFromDIP(widthDp.toFloat())) + } + + override fun setGradientHeight(view: StreamShimmerFrameLayout, heightDp: Double) { + view.setGradientHeight(PixelUtil.toPixelFromDIP(heightDp.toFloat())) + } + + override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) { + super.onDropViewInstance(view) + view.setShimmerEnabled(false) + } + + companion object { + const val REACT_CLASS = "StreamShimmerView" + private const val DEFAULT_BASE_COLOR = 0x00FFFFFF + private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF + private const val DEFAULT_GRADIENT_COLOR = 0xFFFFFFFF.toInt() + } +} diff --git a/package/native-package/package.json b/package/native-package/package.json index a29777ba10..9004a5b23a 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -86,7 +86,7 @@ }, "codegenConfig": { "name": "StreamChatReactNativeSpec", - "type": "modules", + "type": "all", "jsSrcsDir": "src/native" }, "resolutions": { diff --git a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts new file mode 100644 index 0000000000..805c6ad464 --- /dev/null +++ b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts @@ -0,0 +1,17 @@ +import type { ColorValue, HostComponent, ViewProps } from 'react-native'; + +import type { Double } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export interface NativeProps extends ViewProps { + baseColor?: ColorValue; + enabled?: boolean; + gradientColor?: ColorValue; + gradientHeight?: Double; + gradientWidth?: Double; + highlightColor?: ColorValue; +} + +export default codegenNativeComponent( + 'StreamShimmerView', +) as HostComponent; diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index aa99cf3ed5..82ea8e3031 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -74,7 +74,7 @@ export type GalleryPropsWithContext = Pick { const { @@ -444,15 +444,15 @@ const GalleryImageThumbnail = ({ ) : ( <> - - {isLoadingImage ? : null} + {/**/} + {true ? : null} )} diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index b42200c9af..2aa9c1e920 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -1,9 +1,36 @@ import React from 'react'; -import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { NativeShimmerView } from '../UIComponents/NativeShimmerView'; import { ShimmerView } from '../UIComponents/Shimmer/ShimmerView'; export const ImageLoadingIndicator = () => { + const { + theme: { + semantics, + shimmer: { width, height }, + }, + } = useTheme(); + + if (Platform.OS === 'android') { + return ( + + + + + + ); + } + return ( @@ -12,3 +39,11 @@ export const ImageLoadingIndicator = () => { ); }; + +const styles = StyleSheet.create({ + centered: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, +}); diff --git a/package/src/components/UIComponents/NativeShimmerView.android.tsx b/package/src/components/UIComponents/NativeShimmerView.android.tsx new file mode 100644 index 0000000000..ccc7c2d359 --- /dev/null +++ b/package/src/components/UIComponents/NativeShimmerView.android.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { HostComponent, ViewProps, ColorValue } from 'react-native'; + +import type { Double } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type NativeShimmerViewProps = ViewProps & { + baseColor?: ColorValue; + enabled?: boolean; + gradientColor?: ColorValue; + gradientHeight?: Double; + gradientWidth?: Double; + highlightColor?: ColorValue; +}; + +const NativeComponent = codegenNativeComponent( + 'StreamShimmerView', +) as HostComponent; + +export const NativeShimmerView = (props: NativeShimmerViewProps) => { + return ; +}; diff --git a/package/src/components/UIComponents/NativeShimmerView.tsx b/package/src/components/UIComponents/NativeShimmerView.tsx new file mode 100644 index 0000000000..fea13debdc --- /dev/null +++ b/package/src/components/UIComponents/NativeShimmerView.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { ColorValue, View, ViewProps } from 'react-native'; + +export type NativeShimmerViewProps = ViewProps & { + baseColor?: ColorValue; + enabled?: boolean; + gradientColor?: ColorValue; + gradientHeight?: number; + gradientWidth?: number; + highlightColor?: ColorValue; +}; + +export const NativeShimmerView = (props: NativeShimmerViewProps) => { + const { children, ...rest } = props; + return {children}; +}; From 4cf6f6701dc521215220e3928e2463875afc1c6b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 14:54:10 +0100 Subject: [PATCH 04/40] fix: properly interpolate linear gradient --- .../StreamShimmerFrameLayout.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index b0cbdcd6ef..00e28a5c22 100644 --- a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -162,13 +162,33 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) + val softBase = colorWithAlpha(highlightColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) + val mediumBase = colorWithAlpha(highlightColor, MID_HIGHLIGHT_ALPHA_FACTOR) shimmerShader = LinearGradient( 0f, 0f, shimmerWidth, 0f, - intArrayOf(baseColor, highlightColor, baseColor), - floatArrayOf(0f, 0.5f, 1f), + intArrayOf( + baseColor, + softBase, + mediumBase, + highlightColor, + highlightColor, + mediumBase, + softBase, + baseColor, + ), + floatArrayOf( + 0f, + 0.2f, + 0.34f, + 0.44f, + 0.56f, + 0.66f, + 0.8f, + 1f, + ), Shader.TileMode.CLAMP, ) } @@ -207,7 +227,9 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF private const val DEFAULT_GRADIENT_COLOR = Color.WHITE private const val SHIMMER_DURATION_MS = 1200L - private const val SHIMMER_STRIP_WIDTH_RATIO = 0.35f + private const val SHIMMER_STRIP_WIDTH_RATIO = 1.1f + private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.45f + private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.75f private const val GRADIENT_CENTER_ALPHA = 0.35f } } From e952c46b8ac3b3f69280c60f4c91cbdcdf31c67f Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 15:40:16 +0100 Subject: [PATCH 05/40] refactor: solve rn compatibility issues and use shared codebase for expo sdk --- package/expo-package/android/build.gradle | 96 +++++++++++++++++++ .../expo-package/android/gradle.properties | 4 + .../android/src/main/AndroidManifest.xml | 4 + .../streamchatexpo/StreamChatExpoPackage.java | 35 +++++++ package/expo-package/package.json | 6 ++ package/expo-package/react-native.config.js | 10 ++ .../StreamShimmerViewNativeComponent.ts | 17 ++++ package/native-package/android/build.gradle | 8 +- .../StreamShimmerViewNativeComponent.ts | 4 +- .../StreamShimmerFrameLayout.kt | 0 .../StreamShimmerViewManager.kt | 0 11 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 package/expo-package/android/build.gradle create mode 100644 package/expo-package/android/gradle.properties create mode 100644 package/expo-package/android/src/main/AndroidManifest.xml create mode 100644 package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java create mode 100644 package/expo-package/react-native.config.js create mode 100644 package/expo-package/src/native/StreamShimmerViewNativeComponent.ts rename package/{native-package => shared-native}/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt (100%) rename package/{native-package => shared-native}/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt (100%) diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle new file mode 100644 index 0000000000..e12a2c979e --- /dev/null +++ b/package/expo-package/android/build.gradle @@ -0,0 +1,96 @@ +def kotlinVersion = + rootProject.ext.has("kotlinVersion") + ? rootProject.ext.get("kotlinVersion") + : project.properties["StreamChatExpo_kotlinVersion"] + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.2.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger() +} + +android { + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + def agpMajorVersion = agpVersion.tokenize('.')[0].toInteger() + def agpMinorVersion = agpVersion.tokenize('.')[1].toInteger() + + if (agpMajorVersion >= 7 && agpMinorVersion >= 3) { + namespace "com.streamchatexpo" + } + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "17" + } + + sourceSets { + main { + java.srcDirs += ["../../shared-native/android/src/main/java"] + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + "src/newarch", + "${project.buildDir}/generated/source/codegen/java" + ] + } + } + } +} + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation "com.facebook.react:react-native:+" +} + +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../src/") + libraryName = "StreamChatExpo" + codegenJavaPackageName = "com.streamchatexpo" + } +} diff --git a/package/expo-package/android/gradle.properties b/package/expo-package/android/gradle.properties new file mode 100644 index 0000000000..ff43bf1f8d --- /dev/null +++ b/package/expo-package/android/gradle.properties @@ -0,0 +1,4 @@ +StreamChatExpo_kotlinVersion=1.7.0 +StreamChatExpo_minSdkVersion=21 +StreamChatExpo_targetSdkVersion=31 +StreamChatExpo_compileSdkVersion=31 diff --git a/package/expo-package/android/src/main/AndroidManifest.xml b/package/expo-package/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..17df9bf89c --- /dev/null +++ b/package/expo-package/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java b/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java new file mode 100644 index 0000000000..cd42f18297 --- /dev/null +++ b/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java @@ -0,0 +1,35 @@ +package com.streamchatexpo; + +import androidx.annotation.Nullable; +import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.uimanager.ViewManager; +import com.streamchatreactnative.StreamShimmerViewManager; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StreamChatExpoPackage extends TurboReactPackage { + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + return null; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + return moduleInfos; + }; + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.singletonList(new StreamShimmerViewManager()); + } +} diff --git a/package/expo-package/package.json b/package/expo-package/package.json index 1fce674308..62e2b46d50 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -20,6 +20,7 @@ }, "peerDependencies": { "expo": ">=51.0.0", + "react-native": ">=0.76.0", "expo-av": "*", "expo-clipboard": "*", "expo-document-picker": "*", @@ -73,6 +74,11 @@ "prepack": " cp ../../README.md .", "postpack": "rm README.md" }, + "codegenConfig": { + "name": "StreamChatExpoSpec", + "type": "all", + "jsSrcsDir": "src/native" + }, "resolutions": { "@types/react": "^19.0.0" } diff --git a/package/expo-package/react-native.config.js b/package/expo-package/react-native.config.js new file mode 100644 index 0000000000..5159c2e46f --- /dev/null +++ b/package/expo-package/react-native.config.js @@ -0,0 +1,10 @@ +module.exports = { + dependency: { + platforms: { + android: { + packageImportPath: 'import com.streamchatexpo.StreamChatExpoPackage;', + packageInstance: 'new StreamChatExpoPackage()', + }, + }, + }, +}; diff --git a/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts new file mode 100644 index 0000000000..e14e3658c4 --- /dev/null +++ b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts @@ -0,0 +1,17 @@ +import type { ColorValue, HostComponent, ViewProps } from 'react-native'; + +import type { Double, WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export interface NativeProps extends ViewProps { + baseColor?: ColorValue; + enabled?: WithDefault; + gradientColor?: ColorValue; + gradientHeight?: Double; + gradientWidth?: Double; + highlightColor?: ColorValue; +} + +export default codegenNativeComponent( + 'StreamShimmerView', +) as HostComponent; diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 95bdeb9dc7..1df8d6dce1 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -1,3 +1,8 @@ +def kotlinVersion = + rootProject.ext.has("kotlinVersion") + ? rootProject.ext.get("kotlinVersion") + : project.properties["ImageResizer_kotlinVersion"] + buildscript { repositories { google() @@ -6,7 +11,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.2.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${project.properties['ImageResizer_kotlinVersion']}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } @@ -72,6 +77,7 @@ android { sourceSets { main { + java.srcDirs += ["../../shared-native/android/src/main/java"] if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", diff --git a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts index 805c6ad464..e14e3658c4 100644 --- a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts +++ b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts @@ -1,11 +1,11 @@ import type { ColorValue, HostComponent, ViewProps } from 'react-native'; -import type { Double } from 'react-native/Libraries/Types/CodegenTypes'; +import type { Double, WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; export interface NativeProps extends ViewProps { baseColor?: ColorValue; - enabled?: boolean; + enabled?: WithDefault; gradientColor?: ColorValue; gradientHeight?: Double; gradientWidth?: Double; diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt similarity index 100% rename from package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt rename to package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt similarity index 100% rename from package/native-package/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt rename to package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt From 7f54d33ce6da52ff907ed594e29112476660167e Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 15:57:12 +0100 Subject: [PATCH 06/40] fix: properly prepack shared code so it is included in the sdk --- package/expo-package/package.json | 4 +-- package/native-package/package.json | 4 +-- .../scripts/clean-shimmer-native-copies.sh | 33 +++++++++++++++++ package/scripts/sync-shimmer-native.sh | 36 +++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 package/scripts/clean-shimmer-native-copies.sh create mode 100644 package/scripts/sync-shimmer-native.sh diff --git a/package/expo-package/package.json b/package/expo-package/package.json index 62e2b46d50..cf64534d59 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -71,8 +71,8 @@ "expo-audio": "~0.4.6" }, "scripts": { - "prepack": " cp ../../README.md .", - "postpack": "rm README.md" + "prepack": "bash ../scripts/sync-shimmer-native.sh expo-package && cp ../../README.md .", + "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh expo-package" }, "codegenConfig": { "name": "StreamChatExpoSpec", diff --git a/package/native-package/package.json b/package/native-package/package.json index 9004a5b23a..85e6a1d55c 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -78,8 +78,8 @@ } }, "scripts": { - "prepack": " cp ../../README.md .", - "postpack": "rm README.md" + "prepack": "bash ../scripts/sync-shimmer-native.sh native-package && cp ../../README.md .", + "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh native-package" }, "devDependencies": { "react-native": "^0.79.3" diff --git a/package/scripts/clean-shimmer-native-copies.sh b/package/scripts/clean-shimmer-native-copies.sh new file mode 100644 index 0000000000..c9219a35ff --- /dev/null +++ b/package/scripts/clean-shimmer-native-copies.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET="${1:-all}" + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +clean_package() { + local package_name="$1" + local dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + rm -f "$dir/StreamShimmerFrameLayout.kt" "$dir/StreamShimmerViewManager.kt" +} + +case "$TARGET" in + native-package) + clean_package "native-package" + ;; + expo-package) + clean_package "expo-package" + ;; + all) + clean_package "native-package" + clean_package "expo-package" + ;; + *) + echo "Unknown target: $TARGET" + echo "Expected one of: native-package, expo-package, all" + exit 1 + ;; +esac + +echo "Cleaned generated shimmer native copies for target: $TARGET" diff --git a/package/scripts/sync-shimmer-native.sh b/package/scripts/sync-shimmer-native.sh new file mode 100644 index 0000000000..157354d1db --- /dev/null +++ b/package/scripts/sync-shimmer-native.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET="${1:-all}" + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SRC_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" + +copy_to_package() { + local package_name="$1" + local dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + mkdir -p "$dst_dir" + cp "$SRC_DIR/StreamShimmerFrameLayout.kt" "$dst_dir/StreamShimmerFrameLayout.kt" + cp "$SRC_DIR/StreamShimmerViewManager.kt" "$dst_dir/StreamShimmerViewManager.kt" +} + +case "$TARGET" in + native-package) + copy_to_package "native-package" + ;; + expo-package) + copy_to_package "expo-package" + ;; + all) + copy_to_package "native-package" + copy_to_package "expo-package" + ;; + *) + echo "Unknown target: $TARGET" + echo "Expected one of: native-package, expo-package, all" + exit 1 + ;; +esac + +echo "Synchronized shimmer native files for target: $TARGET" From c7423c4c0f52702e7000fa90ff58983ebeaa164f Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 16:20:35 +0100 Subject: [PATCH 07/40] fix: clean up build.gradle files so they reflect file structure --- examples/ExpoMessaging/yarn.lock | 23 ++++++++++++++++----- package/expo-package/.gitignore | 2 ++ package/expo-package/android/build.gradle | 2 -- package/native-package/android/build.gradle | 1 - 4 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 package/expo-package/.gitignore diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index 104da149ca..5513edc778 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -1533,7 +1533,15 @@ resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz#9d5b4b6f23309260a12856cb574c5e64e6c133f7" integrity sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ== -"@gorhom/bottom-sheet@^5.1.6", "@gorhom/bottom-sheet@^5.1.8": +"@gorhom/bottom-sheet@5.1.8": + version "5.1.8" + resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.8.tgz#65547917f5b1dae5a1291dabd4ea8bfee09feba4" + integrity sha512-QuYIVjn3K9bW20n5bgOSjvxBYoWG4YQXiLGOheEAMgISuoT6sMcA270ViSkkb0fenPxcIOwzCnFNuxmr739T9A== + dependencies: + "@gorhom/portal" "1.0.14" + invariant "^2.2.4" + +"@gorhom/bottom-sheet@^5.1.6": version "5.2.6" resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.2.6.tgz#5f2045f6ca965383afe39f7dfa3afad1502b7467" integrity sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ== @@ -4448,6 +4456,11 @@ lodash-es@4.17.21: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash-es@4.17.23: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0" + integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -6052,10 +6065,10 @@ stream-chat-react-native-core@8.1.0: version "0.0.0" uid "" -stream-chat@^9.27.2: - version "9.27.2" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968" - integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg== +stream-chat@^9.35.1: + version "9.35.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.35.1.tgz#d828854a9c27ea7e45e6642d9107966c6606f552" + integrity sha512-649sgO7+llFuW+y/Ja0K4d94aUC+EMxYUVo5mq5AFGT86vUAIXmRIMVHYHA/jw4MYoqfWAFrDK6L9Rhyn/eMkQ== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" diff --git a/package/expo-package/.gitignore b/package/expo-package/.gitignore new file mode 100644 index 0000000000..2dc3e3178e --- /dev/null +++ b/package/expo-package/.gitignore @@ -0,0 +1,2 @@ +# android +android/build diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index e12a2c979e..2f48a8fd8f 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -67,10 +67,8 @@ android { sourceSets { main { - java.srcDirs += ["../../shared-native/android/src/main/java"] if (isNewArchitectureEnabled()) { java.srcDirs += [ - "src/newarch", "${project.buildDir}/generated/source/codegen/java" ] } diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 1df8d6dce1..55ba6503d9 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -77,7 +77,6 @@ android { sourceSets { main { - java.srcDirs += ["../../shared-native/android/src/main/java"] if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", From 9e8b6620ed8cae774b45b86e06726cfb77a3f69c Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 6 Mar 2026 17:21:20 +0100 Subject: [PATCH 08/40] fix: shared files packaging --- package/expo-package/android/build.gradle | 46 +++++++++++++++++++ package/expo-package/package.json | 9 ++++ package/native-package/android/build.gradle | 45 ++++++++++++++++++ .../StreamChatReactNativePackage.java | 1 - package/native-package/package.json | 5 +- package/src/components/Attachment/Gallery.tsx | 18 ++++---- 6 files changed, 113 insertions(+), 11 deletions(-) diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index 2f48a8fd8f..263fe36335 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -30,6 +30,14 @@ def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger() } +def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") +def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") +def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") +def shimmerSourceFiles = ["StreamShimmerFrameLayout.kt", "StreamShimmerViewManager.kt"] +def hasAllShimmerFiles = { File dir -> + shimmerSourceFiles.every { filename -> new File(dir, filename).exists() } +} + android { compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION @@ -67,8 +75,10 @@ android { sourceSets { main { + java.srcDirs += [generatedShimmerSourceDir.absolutePath] if (isNewArchitectureEnabled()) { java.srcDirs += [ + "src/newarch", "${project.buildDir}/generated/source/codegen/java" ] } @@ -76,6 +86,42 @@ android { } } +tasks.register("syncSharedShimmerSources") { + outputs.dir(generatedShimmerSourceDir) + outputs.upToDateWhen { false } + doLast { + def sourceDir = null + if (hasAllShimmerFiles(localShimmerSourceDir)) { + sourceDir = localShimmerSourceDir + } else if (hasAllShimmerFiles(sharedShimmerSourceDir)) { + sourceDir = sharedShimmerSourceDir + } + + if (sourceDir == null) { + throw new GradleException( + "Missing shimmer native sources. Expected either src/main/java/com/streamchatreactnative/*.kt " + + "or ../../shared-native/android/src/main/java/com/streamchatreactnative/*.kt." + ) + } + + generatedShimmerSourceDir.mkdirs() + shimmerSourceFiles.each { filename -> + def srcFile = new File(sourceDir, filename) + def dstFile = new File(generatedShimmerSourceDir, filename) + dstFile.bytes = srcFile.bytes + } + } +} + +tasks.matching { it.name == "preBuild" }.configureEach { + dependsOn("syncSharedShimmerSources") +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + dependsOn("syncSharedShimmerSources") + source(generatedShimmerSourceDir.absolutePath) +} + repositories { mavenCentral() google() diff --git a/package/expo-package/package.json b/package/expo-package/package.json index cf64534d59..d34d641d4c 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -11,6 +11,15 @@ "url": "https://github.com/GetStream/stream-chat-react-native.git", "directory": "package/expo-package" }, + "files": [ + "src", + "types", + "android/src", + "android/build.gradle", + "android/gradle.properties", + "react-native.config.js", + "package.json" + ], "license": "SEE LICENSE IN LICENSE", "main": "src/index.js", "types": "types/index.d.ts", diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 55ba6503d9..d71ac1d249 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -37,6 +37,14 @@ def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger() } +def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") +def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") +def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") +def shimmerSourceFiles = ["StreamShimmerFrameLayout.kt", "StreamShimmerViewManager.kt"] +def hasAllShimmerFiles = { File dir -> + shimmerSourceFiles.every { filename -> new File(dir, filename).exists() } +} + android { compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION @@ -77,6 +85,7 @@ android { sourceSets { main { + java.srcDirs += [generatedShimmerSourceDir.absolutePath] if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", @@ -90,6 +99,42 @@ android { } } +tasks.register("syncSharedShimmerSources") { + outputs.dir(generatedShimmerSourceDir) + outputs.upToDateWhen { false } + doLast { + def sourceDir = null + if (hasAllShimmerFiles(localShimmerSourceDir)) { + sourceDir = localShimmerSourceDir + } else if (hasAllShimmerFiles(sharedShimmerSourceDir)) { + sourceDir = sharedShimmerSourceDir + } + + if (sourceDir == null) { + throw new GradleException( + "Missing shimmer native sources. Expected either src/main/java/com/streamchatreactnative/*.kt " + + "or ../../shared-native/android/src/main/java/com/streamchatreactnative/*.kt." + ) + } + + generatedShimmerSourceDir.mkdirs() + shimmerSourceFiles.each { filename -> + def srcFile = new File(sourceDir, filename) + def dstFile = new File(generatedShimmerSourceDir, filename) + dstFile.bytes = srcFile.bytes + } + } +} + +tasks.matching { it.name == "preBuild" }.configureEach { + dependsOn("syncSharedShimmerSources") +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + dependsOn("syncSharedShimmerSources") + source(generatedShimmerSourceDir.absolutePath) +} + repositories { mavenCentral() google() diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java index 742e03f4ba..9f2decab6d 100644 --- a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java @@ -8,7 +8,6 @@ import com.facebook.react.TurboReactPackage; import com.facebook.react.uimanager.ViewManager; - import java.util.HashMap; import java.util.Collections; import java.util.List; diff --git a/package/native-package/package.json b/package/native-package/package.json index 85e6a1d55c..060f32df35 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -15,7 +15,10 @@ "files": [ "src", "types", - "android", + "android/src", + "android/build.gradle", + "android/gradle.properties", + "android/gradle", "ios", "*.podspec", "package.json" diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 82ea8e3031..9ce0c22066 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -444,15 +444,15 @@ const GalleryImageThumbnail = ({ ) : ( <> - {/**/} - {true ? : null} + + {isLoadingImage ? : null} )} From e62e82eb3722dd753f59e5f54a7f94e8ce08670a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 13:34:27 +0100 Subject: [PATCH 09/40] feat: implement shimmer view on ios as well --- package/expo-package/.gitignore | 6 + package/expo-package/package.json | 10 +- package/expo-package/react-native.config.js | 3 + package/expo-package/src/index.js | 2 + .../optionalDependencies/NativeShimmerView.ts | 3 + .../src/optionalDependencies/index.ts | 1 + package/expo-package/stream-chat-expo.podspec | 19 +++ package/native-package/.gitignore | 6 + package/native-package/package.json | 8 +- package/native-package/src/index.js | 2 + .../optionalDependencies/NativeShimmerView.ts | 3 + .../src/optionalDependencies/index.ts | 1 + .../stream-chat-react-native.podspec | 27 +-- .../scripts/clean-shimmer-native-copies.sh | 5 +- package/scripts/sync-shimmer-native.sh | 18 +- .../shared-native/ios/StreamShimmerView.swift | 155 ++++++++++++++++++ .../ios/StreamShimmerViewComponentView.h | 22 +++ .../ios/StreamShimmerViewComponentView.mm | 92 +++++++++++ .../Attachment/ImageLoadingIndicator.tsx | 6 +- .../NativeShimmerView.android.tsx | 21 +-- .../UIComponents/NativeShimmerView.ios.tsx | 25 +++ .../UIComponents/NativeShimmerView.tsx | 7 + package/src/native.ts | 30 +++- 23 files changed, 425 insertions(+), 47 deletions(-) create mode 100644 package/expo-package/src/optionalDependencies/NativeShimmerView.ts create mode 100644 package/expo-package/stream-chat-expo.podspec create mode 100644 package/native-package/src/optionalDependencies/NativeShimmerView.ts create mode 100644 package/shared-native/ios/StreamShimmerView.swift create mode 100644 package/shared-native/ios/StreamShimmerViewComponentView.h create mode 100644 package/shared-native/ios/StreamShimmerViewComponentView.mm create mode 100644 package/src/components/UIComponents/NativeShimmerView.ios.tsx diff --git a/package/expo-package/.gitignore b/package/expo-package/.gitignore index 2dc3e3178e..6cba831d87 100644 --- a/package/expo-package/.gitignore +++ b/package/expo-package/.gitignore @@ -1,2 +1,8 @@ # android android/build + +# ios +ios/build +ios/StreamShimmerView.swift +ios/StreamShimmerViewComponentView.h +ios/StreamShimmerViewComponentView.mm diff --git a/package/expo-package/package.json b/package/expo-package/package.json index d34d641d4c..af3012118a 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -17,6 +17,8 @@ "android/src", "android/build.gradle", "android/gradle.properties", + "ios", + "*.podspec", "react-native.config.js", "package.json" ], @@ -80,13 +82,19 @@ "expo-audio": "~0.4.6" }, "scripts": { + "postinstall": "if [ -f ../scripts/sync-shimmer-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shimmer-native.sh expo-package; fi", "prepack": "bash ../scripts/sync-shimmer-native.sh expo-package && cp ../../README.md .", "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh expo-package" }, "codegenConfig": { "name": "StreamChatExpoSpec", "type": "all", - "jsSrcsDir": "src/native" + "jsSrcsDir": "src/native", + "ios": { + "componentProvider": { + "StreamShimmerView": "StreamShimmerViewComponentView" + } + } }, "resolutions": { "@types/react": "^19.0.0" diff --git a/package/expo-package/react-native.config.js b/package/expo-package/react-native.config.js index 5159c2e46f..a94bce2790 100644 --- a/package/expo-package/react-native.config.js +++ b/package/expo-package/react-native.config.js @@ -5,6 +5,9 @@ module.exports = { packageImportPath: 'import com.streamchatexpo.StreamChatExpoPackage;', packageInstance: 'new StreamChatExpoPackage()', }, + ios: { + podspecPath: 'stream-chat-expo.podspec', + }, }, }, }; diff --git a/package/expo-package/src/index.js b/package/expo-package/src/index.js index f9fbff91d6..53960194f9 100644 --- a/package/expo-package/src/index.js +++ b/package/expo-package/src/index.js @@ -10,6 +10,7 @@ import { getLocalAssetUri, getPhotos, iOS14RefreshGallerySelection, + NativeShimmerView, oniOS14GalleryLibrarySelectionChange, overrideAudioRecordingConfiguration, pickDocument, @@ -31,6 +32,7 @@ registerNativeHandlers({ getLocalAssetUri, getPhotos, iOS14RefreshGallerySelection, + NativeShimmerView, oniOS14GalleryLibrarySelectionChange, overrideAudioRecordingConfiguration, pickDocument, diff --git a/package/expo-package/src/optionalDependencies/NativeShimmerView.ts b/package/expo-package/src/optionalDependencies/NativeShimmerView.ts new file mode 100644 index 0000000000..fbbe898275 --- /dev/null +++ b/package/expo-package/src/optionalDependencies/NativeShimmerView.ts @@ -0,0 +1,3 @@ +import StreamShimmerViewNativeComponent from '../native/StreamShimmerViewNativeComponent'; + +export const NativeShimmerView = StreamShimmerViewNativeComponent; diff --git a/package/expo-package/src/optionalDependencies/index.ts b/package/expo-package/src/optionalDependencies/index.ts index 86122fb6f1..5cc6f86346 100644 --- a/package/expo-package/src/optionalDependencies/index.ts +++ b/package/expo-package/src/optionalDependencies/index.ts @@ -3,6 +3,7 @@ export * from './deleteFile'; export * from './getLocalAssetUri'; export * from './getPhotos'; export * from './iOS14RefreshGallerySelection'; +export * from './NativeShimmerView'; export * from './oniOS14GalleryLibrarySelectionChange'; export * from './pickDocument'; export * from './pickImage'; diff --git a/package/expo-package/stream-chat-expo.podspec b/package/expo-package/stream-chat-expo.podspec new file mode 100644 index 0000000000..5b6701c9dc --- /dev/null +++ b/package/expo-package/stream-chat-expo.podspec @@ -0,0 +1,19 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "stream-chat-expo" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://www.npmjs.com/package/stream-chat-expo" + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "./ios", :tag => "#{s.version}" } + s.source_files = "ios/**/*.{h,m,mm,swift}" + s.private_header_files = "ios/**/*.h" + + install_modules_dependencies(s) +end diff --git a/package/native-package/.gitignore b/package/native-package/.gitignore index 2dc3e3178e..6cba831d87 100644 --- a/package/native-package/.gitignore +++ b/package/native-package/.gitignore @@ -1,2 +1,8 @@ # android android/build + +# ios +ios/build +ios/StreamShimmerView.swift +ios/StreamShimmerViewComponentView.h +ios/StreamShimmerViewComponentView.mm diff --git a/package/native-package/package.json b/package/native-package/package.json index 060f32df35..9a779c5b91 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -81,6 +81,7 @@ } }, "scripts": { + "postinstall": "if [ -f ../scripts/sync-shimmer-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shimmer-native.sh native-package; fi", "prepack": "bash ../scripts/sync-shimmer-native.sh native-package && cp ../../README.md .", "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh native-package" }, @@ -90,7 +91,12 @@ "codegenConfig": { "name": "StreamChatReactNativeSpec", "type": "all", - "jsSrcsDir": "src/native" + "jsSrcsDir": "src/native", + "ios": { + "componentProvider": { + "StreamShimmerView": "StreamShimmerViewComponentView" + } + } }, "resolutions": { "@types/react": "^19.0.0" diff --git a/package/native-package/src/index.js b/package/native-package/src/index.js index c782abf541..ee090a1cc6 100644 --- a/package/native-package/src/index.js +++ b/package/native-package/src/index.js @@ -11,6 +11,7 @@ import { getLocalAssetUri, getPhotos, iOS14RefreshGallerySelection, + NativeShimmerView, oniOS14GalleryLibrarySelectionChange, overrideAudioRecordingConfiguration, pickDocument, @@ -32,6 +33,7 @@ registerNativeHandlers({ getLocalAssetUri, getPhotos, iOS14RefreshGallerySelection, + NativeShimmerView, oniOS14GalleryLibrarySelectionChange, overrideAudioRecordingConfiguration, pickDocument, diff --git a/package/native-package/src/optionalDependencies/NativeShimmerView.ts b/package/native-package/src/optionalDependencies/NativeShimmerView.ts new file mode 100644 index 0000000000..fbbe898275 --- /dev/null +++ b/package/native-package/src/optionalDependencies/NativeShimmerView.ts @@ -0,0 +1,3 @@ +import StreamShimmerViewNativeComponent from '../native/StreamShimmerViewNativeComponent'; + +export const NativeShimmerView = StreamShimmerViewNativeComponent; diff --git a/package/native-package/src/optionalDependencies/index.ts b/package/native-package/src/optionalDependencies/index.ts index 776ef08ba7..1b1ddee508 100644 --- a/package/native-package/src/optionalDependencies/index.ts +++ b/package/native-package/src/optionalDependencies/index.ts @@ -4,6 +4,7 @@ export * from './FlatList'; export * from './getLocalAssetUri'; export * from './getPhotos'; export * from './iOS14RefreshGallerySelection'; +export * from './NativeShimmerView'; export * from './oniOS14GalleryLibrarySelectionChange'; export * from './pickDocument'; export * from './pickImage'; diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec index 32f974ca8a..698d90f4d5 100644 --- a/package/native-package/stream-chat-react-native.podspec +++ b/package/native-package/stream-chat-react-native.podspec @@ -1,7 +1,6 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| s.name = "stream-chat-react-native" @@ -11,28 +10,10 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "10.0" } + s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "./ios", :tag => "#{s.version}" } + s.source_files = "ios/**/*.{h,m,mm,swift}" + s.private_header_files = "ios/**/*.h" - s.source_files = "ios/**/*.{h,m,mm}" - - s.dependency "React-Core" - s.ios.framework = 'AssetsLibrary', 'MobileCoreServices' - - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - install_modules_dependencies(s) - end + install_modules_dependencies(s) end - diff --git a/package/scripts/clean-shimmer-native-copies.sh b/package/scripts/clean-shimmer-native-copies.sh index c9219a35ff..22b7779a8b 100644 --- a/package/scripts/clean-shimmer-native-copies.sh +++ b/package/scripts/clean-shimmer-native-copies.sh @@ -8,8 +8,9 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" clean_package() { local package_name="$1" - local dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" - rm -f "$dir/StreamShimmerFrameLayout.kt" "$dir/StreamShimmerViewManager.kt" + local android_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + + rm -f "$android_dir/StreamShimmerFrameLayout.kt" "$android_dir/StreamShimmerViewManager.kt" } case "$TARGET" in diff --git a/package/scripts/sync-shimmer-native.sh b/package/scripts/sync-shimmer-native.sh index 157354d1db..6a22b3f071 100644 --- a/package/scripts/sync-shimmer-native.sh +++ b/package/scripts/sync-shimmer-native.sh @@ -5,14 +5,22 @@ set -euo pipefail TARGET="${1:-all}" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -SRC_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" +ANDROID_SRC_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" +IOS_SRC_DIR="$ROOT_DIR/shared-native/ios" copy_to_package() { local package_name="$1" - local dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" - mkdir -p "$dst_dir" - cp "$SRC_DIR/StreamShimmerFrameLayout.kt" "$dst_dir/StreamShimmerFrameLayout.kt" - cp "$SRC_DIR/StreamShimmerViewManager.kt" "$dst_dir/StreamShimmerViewManager.kt" + local android_dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + local ios_dst_dir="$ROOT_DIR/$package_name/ios" + + mkdir -p "$android_dst_dir" + cp "$ANDROID_SRC_DIR/StreamShimmerFrameLayout.kt" "$android_dst_dir/StreamShimmerFrameLayout.kt" + cp "$ANDROID_SRC_DIR/StreamShimmerViewManager.kt" "$android_dst_dir/StreamShimmerViewManager.kt" + + mkdir -p "$ios_dst_dir" + cp "$IOS_SRC_DIR/StreamShimmerViewComponentView.h" "$ios_dst_dir/StreamShimmerViewComponentView.h" + cp "$IOS_SRC_DIR/StreamShimmerViewComponentView.mm" "$ios_dst_dir/StreamShimmerViewComponentView.mm" + cp "$IOS_SRC_DIR/StreamShimmerView.swift" "$ios_dst_dir/StreamShimmerView.swift" } case "$TARGET" in diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift new file mode 100644 index 0000000000..9f2a73da7d --- /dev/null +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -0,0 +1,155 @@ +import QuartzCore +import UIKit + +@objcMembers +public final class StreamShimmerView: UIView { + private static let edgeHighlightAlpha: CGFloat = 0.45 + private static let midHighlightAlpha: CGFloat = 0.75 + private static let centerGradientAlpha: CGFloat = 0.35 + private static let shimmerStripWidthRatio: CGFloat = 1.1 + private static let shimmerDuration: CFTimeInterval = 1.2 + private static let shimmerAnimationKey = "stream_shimmer_translate_x" + + private let baseLayer = CALayer() + private let shimmerLayer = CAGradientLayer() + private let centerGradientLayer = CAGradientLayer() + + private var baseColor: UIColor = .clear + private var highlightColor: UIColor = UIColor(white: 1, alpha: centerGradientAlpha) + private var gradientColor: UIColor = .white + private var gradientWidth: CGFloat = 0 + private var gradientHeight: CGFloat = 0 + private var enabled = false + + public override init(frame: CGRect) { + super.init(frame: frame) + setupLayers() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setupLayers() + } + + public override func layoutSubviews() { + super.layoutSubviews() + updateLayersForCurrentState() + } + + public func apply( + baseColor: UIColor, + highlightColor: UIColor, + gradientColor: UIColor, + gradientWidth: CGFloat, + gradientHeight: CGFloat, + enabled: Bool + ) { + self.baseColor = baseColor + self.highlightColor = highlightColor + self.gradientColor = gradientColor + self.gradientWidth = max(gradientWidth, 0) + self.gradientHeight = max(gradientHeight, 0) + self.enabled = enabled + updateLayersForCurrentState() + } + + public func stopAnimation() { + shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey) + } + + private func setupLayers() { + isUserInteractionEnabled = false + + shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5) + shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5) + shimmerLayer.locations = [0.0, 0.2, 0.34, 0.44, 0.56, 0.66, 0.8, 1.0] + + centerGradientLayer.startPoint = CGPoint(x: 0, y: 0.5) + centerGradientLayer.endPoint = CGPoint(x: 1, y: 0.5) + centerGradientLayer.locations = [0.0, 0.5, 1.0] + + layer.addSublayer(baseLayer) + layer.addSublayer(shimmerLayer) + layer.addSublayer(centerGradientLayer) + } + + private func updateLayersForCurrentState() { + let bounds = self.bounds + guard !bounds.isEmpty else { + stopAnimation() + return + } + + baseLayer.frame = bounds + baseLayer.backgroundColor = baseColor.cgColor + + updateShimmerLayer(for: bounds) + updateCenterGradientLayer(for: bounds) + updateShimmerAnimation(for: bounds) + } + + private func updateShimmerLayer(for bounds: CGRect) { + let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) + shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height) + shimmerLayer.colors = [ + baseColor.cgColor, + color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, + color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, + highlightColor.cgColor, + highlightColor.cgColor, + color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, + color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, + baseColor.cgColor, + ] + shimmerLayer.isHidden = !enabled + } + + private func updateCenterGradientLayer(for bounds: CGRect) { + guard gradientWidth > 0, gradientHeight > 0 else { + centerGradientLayer.isHidden = true + return + } + + let left = (bounds.width - gradientWidth) / 2 + let top = (bounds.height - gradientHeight) / 2 + centerGradientLayer.frame = CGRect(x: left, y: top, width: gradientWidth, height: gradientHeight) + centerGradientLayer.colors = [ + color(gradientColor, alphaFactor: 0).cgColor, + color(gradientColor, alphaFactor: Self.centerGradientAlpha).cgColor, + color(gradientColor, alphaFactor: 0).cgColor, + ] + centerGradientLayer.isHidden = false + } + + private func updateShimmerAnimation(for bounds: CGRect) { + guard enabled, bounds.width > 0, bounds.height > 0 else { + stopAnimation() + return + } + + if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil { + return + } + + let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) + let animation = CABasicAnimation(keyPath: "position.x") + animation.fromValue = -shimmerWidth / 2 + animation.toValue = bounds.width + shimmerWidth / 2 + animation.duration = Self.shimmerDuration + animation.repeatCount = .infinity + animation.isRemovedOnCompletion = false + animation.fillMode = .forwards + shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey) + } + + private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + guard color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { + return color + } + return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor) + } +} diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.h b/package/shared-native/ios/StreamShimmerViewComponentView.h new file mode 100644 index 0000000000..5fd2863c5c --- /dev/null +++ b/package/shared-native/ios/StreamShimmerViewComponentView.h @@ -0,0 +1,22 @@ +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#endif + +#ifndef StreamShimmerViewComponentView_h +#define StreamShimmerViewComponentView_h + +NS_ASSUME_NONNULL_BEGIN + +@interface StreamShimmerViewComponentView : +#ifdef RCT_NEW_ARCH_ENABLED + RCTViewComponentView +#else + UIView +#endif +@end + +NS_ASSUME_NONNULL_END + +#endif /* StreamShimmerViewComponentView_h */ diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm new file mode 100644 index 0000000000..e0961c4ad9 --- /dev/null +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -0,0 +1,92 @@ +#import "StreamShimmerViewComponentView.h" + +#ifdef RCT_NEW_ARCH_ENABLED + +#if __has_include() +#import +#import +#import +#elif __has_include() +#import +#import +#import +#else +#error "Unable to find generated codegen headers for StreamShimmerView." +#endif + +#if __has_include() +#import +#elif __has_include() +#import +#elif __has_include("stream_chat_react_native-Swift.h") +#import "stream_chat_react_native-Swift.h" +#elif __has_include("stream_chat_expo-Swift.h") +#import "stream_chat_expo-Swift.h" +#else +#error "Unable to import generated Swift header for StreamShimmerView." +#endif + +#import + +using namespace facebook::react; + +@interface StreamShimmerViewComponentView () +@end + +@implementation StreamShimmerViewComponentView { + StreamShimmerView *_shimmerView; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _shimmerView = [[StreamShimmerView alloc] initWithFrame:self.bounds]; + _shimmerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self insertSubview:_shimmerView atIndex:0]; + } + + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self sendSubviewToBack:_shimmerView]; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &newProps = *std::static_pointer_cast(props); + + UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor clearColor]; + UIColor *highlightColor = + RCTUIColorFromSharedColor(newProps.highlightColor) ?: [UIColor colorWithWhite:1 alpha:0.35]; + UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor]; + + [_shimmerView applyWithBaseColor:baseColor + highlightColor:highlightColor + gradientColor:gradientColor + gradientWidth:(CGFloat)MAX(newProps.gradientWidth, 0) + gradientHeight:(CGFloat)MAX(newProps.gradientHeight, 0) + enabled:newProps.enabled]; + + [super updateProps:props oldProps:oldProps]; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + [_shimmerView stopAnimation]; +} + +@end + +#endif diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index 2aa9c1e920..d486d9ac45 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -13,12 +13,12 @@ export const ImageLoadingIndicator = () => { }, } = useTheme(); - if (Platform.OS === 'android') { + if (Platform.OS === 'android' || Platform.OS === 'ios') { return ( ( - 'StreamShimmerView', -) as HostComponent; - export const NativeShimmerView = (props: NativeShimmerViewProps) => { - return ; + const NativeShimmerComponent = NativeHandlers.NativeShimmerView; + if (NativeShimmerComponent) { + return ; + } + + const { children, ...rest } = props; + return {children}; }; diff --git a/package/src/components/UIComponents/NativeShimmerView.ios.tsx b/package/src/components/UIComponents/NativeShimmerView.ios.tsx new file mode 100644 index 0000000000..37180d5e84 --- /dev/null +++ b/package/src/components/UIComponents/NativeShimmerView.ios.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { ColorValue, View, ViewProps } from 'react-native'; + +import { NativeHandlers } from '../../native'; + +export type NativeShimmerViewProps = ViewProps & { + baseColor?: ColorValue; + enabled?: boolean; + gradientColor?: ColorValue; + gradientHeight?: number; + gradientWidth?: number; + highlightColor?: ColorValue; +}; + +export const NativeShimmerView = (props: NativeShimmerViewProps) => { + const NativeShimmerComponent = NativeHandlers.NativeShimmerView; + console.log('SHIMMER ME: ', NativeShimmerComponent); + if (NativeShimmerComponent) { + console.log('SHIMMER ME TIMBERS !'); + return ; + } + + const { children, ...rest } = props; + return {children}; +}; diff --git a/package/src/components/UIComponents/NativeShimmerView.tsx b/package/src/components/UIComponents/NativeShimmerView.tsx index fea13debdc..7cf2dd3ac8 100644 --- a/package/src/components/UIComponents/NativeShimmerView.tsx +++ b/package/src/components/UIComponents/NativeShimmerView.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { ColorValue, View, ViewProps } from 'react-native'; +import { NativeHandlers } from '../../native'; + export type NativeShimmerViewProps = ViewProps & { baseColor?: ColorValue; enabled?: boolean; @@ -11,6 +13,11 @@ export type NativeShimmerViewProps = ViewProps & { }; export const NativeShimmerView = (props: NativeShimmerViewProps) => { + const NativeShimmerComponent = NativeHandlers.NativeShimmerView; + if (NativeShimmerComponent) { + return ; + } + const { children, ...rest } = props; return {children}; }; diff --git a/package/src/native.ts b/package/src/native.ts index c05e7f43de..b871150f2e 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -1,5 +1,11 @@ import type React from 'react'; -import { FlatList as DefaultFlatList, StyleProp, ViewStyle } from 'react-native'; +import { + ColorValue, + FlatList as DefaultFlatList, + StyleProp, + ViewProps, + ViewStyle, +} from 'react-native'; import type { File } from './types/types'; const fail = () => { @@ -287,6 +293,15 @@ export type VideoType = { rate?: number; }; +export type NativeShimmerViewProps = ViewProps & { + baseColor?: ColorValue; + enabled?: boolean; + gradientColor?: ColorValue; + gradientHeight?: number; + gradientWidth?: number; + highlightColor?: ColorValue; +}; + type Handlers = { Audio?: AudioType; compressImage?: CompressImage; @@ -306,12 +321,16 @@ type Handlers = { setClipboardString?: SetClipboardString; shareImage?: ShareImage; Sound?: SoundType; + NativeShimmerView?: React.ComponentType; takePhoto?: TakePhoto; triggerHaptic?: TriggerHaptic; Video?: React.ComponentType; }; -export const NativeHandlers: Pick & +export const NativeHandlers: Pick< + Handlers, + 'Audio' | 'FlatList' | 'NativeShimmerView' | 'Video' | 'Sound' +> & Required< Pick< Handlers, @@ -346,6 +365,7 @@ export const NativeHandlers: Pick { NativeHandlers.Sound = handlers.Sound; } + console.log('CHECK SHIMMER', handlers.NativeShimmerView); + if (handlers.NativeShimmerView !== undefined) { + console.log('SHIMMER HERE ?!'); + NativeHandlers.NativeShimmerView = handlers.NativeShimmerView; + } + if (handlers.takePhoto !== undefined) { NativeHandlers.takePhoto = handlers.takePhoto; } From 4fc2b8d1dc767549f6b61e3cc89bf3cc49dd9453 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 15:23:11 +0100 Subject: [PATCH 10/40] fix: copying ios files from clean slate and scripts --- examples/SampleApp/ios/Podfile.lock | 228 +++++++++++------- examples/SampleApp/package.json | 4 + package/expo-package/stream-chat-expo.podspec | 5 + .../stream-chat-react-native.podspec | 5 + 4 files changed, 159 insertions(+), 83 deletions(-) diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index 61fc07eb9c..dc6d618e46 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -289,7 +289,6 @@ PODS: - React-RCTText (= 0.80.2) - React-RCTVibration (= 0.80.2) - React-callinvoker (0.80.2) - - React-Codegen (0.1.0) - React-Core (0.80.2): - boost - DoubleConversion @@ -1920,6 +1919,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - react-native-blur (4.4.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-cameraroll (7.10.0): - boost - DoubleConversion @@ -2722,6 +2750,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - RNCClipboard (1.16.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RNFastImage (8.6.3): - React-Core - SDWebImage (~> 5.11.1) @@ -3146,7 +3203,6 @@ PODS: - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - - React-Codegen - React-Core - React-debug - React-Fabric @@ -3275,6 +3331,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) + - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)" - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" @@ -3315,6 +3372,7 @@ DEPENDENCIES: - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - RNFastImage (from `../node_modules/react-native-fast-image`) - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" @@ -3351,7 +3409,6 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift - - React-Codegen - SDWebImage - SDWebImageWebPCoder - SocketRocket @@ -3446,6 +3503,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" react-native-blob-util: :path: "../node_modules/react-native-blob-util" + react-native-blur: + :path: "../node_modules/@react-native-community/blur" react-native-cameraroll: :path: "../node_modules/@react-native-camera-roll/camera-roll" react-native-document-picker: @@ -3526,6 +3585,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" RNFastImage: :path: "../node_modules/react-native-fast-image" RNFBApp: @@ -3579,104 +3640,105 @@ SPEC CHECKSUMS: hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - NitroModules: 01ae20fc1e8fc9a3b088ab8ed06ab92527a04f0d - NitroSound: 347b6a21f2e7d5601c92ef81cec7836f8f8be44c - op-sqlite: a7e46cfdaebeef219fd0e939332967af9fe6d406 + NitroModules: 8849240f6ee6d3b295514112437e3b09e855cb67 + NitroSound: fe46960c89410e62e05e9a709d8bf28a8202d1b3 + op-sqlite: b9a4028bef60145d7b98fbbc4341c83420cdcfd5 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7 RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4 RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f React: e7a4655b09d0e17e54be188cc34c2f3e2087318a React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b - React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542 - React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553 - React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691 + React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7 + React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2 + React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac React-debug: ab52e07f7148480ea61c5e9b68408d749c6e2b8f - React-defaultsnativemodule: 291d2b0a93c399056121f4f0acc7f46d155a38ec - React-domnativemodule: c4968302e857bd422df8eec50a3cd4d078bd4ac0 - React-Fabric: 7e3ba48433b87a416052c4077d5965aff64cb8c9 - React-FabricComponents: 21de255cf52232644d12d3288cced1f0c519b25d - React-FabricImage: 15a0961a0ab34178f1c803aa0a7d28f21322ffc3 - React-featureflags: 4e5dad365d57e3c3656447dfdad790f75878d9f4 - React-featureflagsnativemodule: 5eac59389131c2b87d165dac4094b5e86067fabb - React-graphics: 2f9b3db89f156afd793da99f23782f400f58c1ee - React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761 - React-idlecallbacksnativemodule: 1d7e1f73b624926d16db956e87c4885ef485664a - React-ImageManager: 8b6066f6638fba7d4a94fbd0b39b477ea8aced58 - React-jserrorhandler: e5a4626d65b0eda9a11c43a9f14d0423d8a7080d - React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c - React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826 - React-jsinspector: 094e3cb99952a0024fa977fa04706e68747cba18 - React-jsinspectorcdp: dca545979146e3ecbdc999c0789dab55beecc140 - React-jsinspectornetwork: 0a105fe74b0b1a93f70409d955c8a0556dc17c59 - React-jsinspectortracing: 76088dd78a2de3ea637a860cdf39a6d9c2637d6b - React-jsitooling: a2e1e87382aae2294bc94a6e9682b9bc83da1d36 - React-jsitracing: 45827be59e673f4c54175c150891301138846906 - React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce - React-Mapbuffer: 8f620d1794c6b59a8c3862c3ae820a2e9e6c9bb0 - React-microtasksnativemodule: dcf5321c9a41659a6718df8a5f202af1577c6825 - react-native-blob-util: a511afccff6511544ebf56928e6afdf837b037a7 - react-native-cameraroll: 8c3ba9b6f511cf645778de19d5039b61d922fdfb - react-native-document-picker: b37cf6660ad9087b782faa78a1e67687fac15bfd - react-native-geolocation: b7f68b8c04e36ee669c630dbc48dd42cf93a0a41 - react-native-image-picker: 9bceb747cd6cde22a3416ffdc819d11b5b11f156 - react-native-maps: 9febd31278b35cd21e4fad2cf6fa708993be5dab - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-safe-area-context: 32293dc61d1b92ccf892499ab6f8acfd609f9aef - react-native-video: 4da16bfca01a02aa2095e40683d74f2d6563207c - React-NativeModulesApple: 342e280bb9fc2aa5f61f6c257b309a86b995e12d + React-defaultsnativemodule: fab7bf1b5ce1f3ed252aa4e949ec48a8df67d624 + React-domnativemodule: 735fa5238cceebceeecc18f9f4321016461178cf + React-Fabric: c75719fc8818049c3cf9071f0619af988b020c9f + React-FabricComponents: 74a381cc0dfaf2ec3ee29f39ef8533a7fd864b83 + React-FabricImage: 9a3ff143b1ac42e077c0b33ec790f3674ace5783 + React-featureflags: e1eca0727563a61c919131d57bbd0019c3bdb0f0 + React-featureflagsnativemodule: 692211fd48283b2ddee3817767021010e2f1788e + React-graphics: 759b02bde621f12426c1dc1ae2498aaa6b441cd7 + React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b + React-idlecallbacksnativemodule: 731552efc0815fc9d65a6931da55e722b1b3a397 + React-ImageManager: 2c510a480f2c358f56a82df823c66d5700949c96 + React-jserrorhandler: 411e18cbdcbdf546f8f8707091faeb00703527c1 + React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9 + React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb + React-jsinspector: 2f0751e6a4fb840f4ed325384d0795a9e9afaf39 + React-jsinspectorcdp: 71c48944d97f5f20e8e144e419ddf04ffa931e93 + React-jsinspectornetwork: 102f347669b278644cc9bb4ebf2f90422bd5ccef + React-jsinspectortracing: 0f6f2ec7f3faa9dc73d591b24b460141612515eb + React-jsitooling: b557f8e12efdaf16997e43b0d07dbd8a3fce3a5b + React-jsitracing: f9a77561d99c0cd053a8230bab4829b100903949 + React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb + React-Mapbuffer: 230c34b1cabd1c4815726c711b9df221c3d3fbfb + React-microtasksnativemodule: 29d62f132e4aba34ebb7f2b936dde754eb08971b + react-native-blob-util: cbd6b292d0f558f09dce85e6afe68074cd031f3e + react-native-blur: 92cc889e5515dcb0746d5a24207a28eea2f6bb65 + react-native-cameraroll: 00057cc0ec595fdbdf282ecfb931d484b240565f + react-native-document-picker: c5fa18e9fc47b34cfbab3b0a4447d0df918a5621 + react-native-geolocation: eb39c815c9b58ddc3efb552cafdd4b035e4cf682 + react-native-image-picker: e479ec8884df9d99a62c1f53f2307055ad43ea85 + react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225 + react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac + react-native-safe-area-context: 7fd4c2c8023da8e18eaa3424cb49d52f626debee + react-native-video: 71973843c2c9ac154c54f95a5a408fd8b041790e + React-NativeModulesApple: d061f458c3febdf0ac99b1b0faf23b7305974b25 React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1 - React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9 - React-performancetimeline: 3ac316a346fe3d48801a746b754dd8f5b5146838 + React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f + React-performancetimeline: 53bdf62ff49a9b0c4bd4d66329fdcf28d77c1c9d React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e - React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d - React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251 - React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b - React-RCTFabric: 04d1cf11ee3747a699260492e319e92649d7ac88 - React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae - React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab - React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670 - React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc - React-RCTRuntime: 52b28e281aba881e1f94ee8b16611823b730d1c5 - React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307 - React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9 - React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72 + React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89 + React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb + React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0 + React-RCTFabric: fad6230640c42fb8cda29b1d0759f7a1fb8cc677 + React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec + React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9 + React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce + React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482 + React-RCTRuntime: c69b86dc60dcc7297318097fc60bd8e40b050f74 + React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884 + React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74 + React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440 React-rendererconsistency: aba18fa58a4d037004f6bed6bb201eb368016c56 - React-renderercss: b490bd53486a6bae1e9809619735d1f2b2cabd7f - React-rendererdebug: 8db25b276b64d5a1dbf05677de0c4ff1039d5184 + React-renderercss: c7c140782f5f21103b638abfde7b3f11d6a5fd7e + React-rendererdebug: 111519052db9610f1b93baf7350c800621df3d0c React-rncore: 22f344c7f9109b68c3872194b0b5081ca1aee655 - React-RuntimeApple: 19bdabedda0eeb70acb71e68bfc42d61bbcbacd9 - React-RuntimeCore: 11bf03bdbd6e72857481c46d0f4eb9c19b14c754 + React-RuntimeApple: 30d20d804a216eb297ccc9ce1dc9e931738c03b1 + React-RuntimeCore: 6e1facd50e0b7aed1bc36b090015f33133958bb6 React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5 - React-RuntimeHermes: d8f736d0a2d38233c7ec7bd36040eb9b0a3ccd8c - React-runtimescheduler: 0c95966d030c8ebbebddaab49630cda2889ca073 + React-RuntimeHermes: 222268a5931a23f095565c4d60e2673c04e2178a + React-runtimescheduler: aea93219348ba3069fe6c7685a84fe17d3a4b4ee React-timing: 42e8212c479d1e956d3024be0a07f205a2e34d9d - React-utils: a185f723baa0c525c361e6c281a846d919623dbe - ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0 - ReactCodegen: 4928682e20747464165effacc170019a18da953c - ReactCommon: ec1cdf708729338070f8c4ad746768a782fd9eb1 - RNCAsyncStorage: f30b3a83064e28b0fc46f1fbd3834589ed64c7b9 - RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87 - RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e - RNFBMessaging: de62448d205095171915d622ed5fb45c2be5e075 - RNGestureHandler: 0c0d36c0f3c17fc755543fad1c182e1cd541f898 - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 - RNReactNativeHapticFeedback: d39b9a5b334ce26f49ca6abe9eea8b3938532aee - RNReanimated: e3dd9527a9614e1c9e127018cca9486f2c13b2a9 - RNScreens: 6a2d1ff4d263d29d3d3db9f3c19aad2f99fdd162 - RNShare: 9d801eafd9ae835f51bcae6b5c8de9bf3389075b - RNSVG: bc7ccfe884848ac924d2279d9025d41b5f05cb0c - RNWorklets: 4bd2a43ae826633e5e0a92953fce2eb8265759d4 + React-utils: 0ebf25dd4eb1b5497933f4d8923b862d0fe9566f + ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989 + ReactCodegen: f8ae44cfcb65af88f409f4b094b879591232f12c + ReactCommon: a237545f08f598b8b37dc3a9163c1c4b85817b0b + RNCAsyncStorage: afe7c3711dc256e492aa3a50dcac43eecebd0ae5 + RNCClipboard: f09308447254536ab20b243607766796efdf210c + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 + RNFBApp: df5caad9f64b6bc87f8a0b110e6bc411fb00a12b + RNFBMessaging: 6586f18ab3411aeb3349088c19fe54283d39e529 + RNGestureHandler: 6f2448500f339bc93dc589a5fd4da00e6d0033b2 + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 + RNReactNativeHapticFeedback: 8bd4a2ba7c3daeb5d2acfceb8b61743f203076d0 + RNReanimated: 0d996442e746ee9d947f13914fcc17b43b17c445 + RNScreens: 5ca475eb30f4362c5808f3ff4a1c7b786bcd878e + RNShare: 083f05646b9534305999cf20452bd87ca0e8b0b0 + RNSVG: fd433fe5da0f7fee8c78f5865f29ab37321dbd7f + RNWorklets: a382db09224b0f6fda1c72c4c9bf1abfac9b3db8 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: f42e234640869e0eafcdd354441414ad1818b9fe - Teleport: c089481dd2bb020e3dced39b7f8849b93d1499f6 + stream-chat-react-native: 4c7b4132ab6b45c016273a35e465583c3cd8b151 + Teleport: 9ae328b3384204b23236e47bd0d8e994ce6bdb48 Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 20836e2dc4..42cba74738 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -7,8 +7,12 @@ "url": "https://github.com/GetStream/stream-chat-react-native.git" }, "scripts": { + "sync-native": "bash ../../package/scripts/sync-shimmer-native.sh native-package", + "preandroid": "yarn sync-native", "android": "react-native run-android", + "preios": "yarn sync-native", "ios": "react-native run-ios", + "prestart": "yarn sync-native", "start": "react-native start", "test": "jest", "lint": "eslint .", diff --git a/package/expo-package/stream-chat-expo.podspec b/package/expo-package/stream-chat-expo.podspec index 5b6701c9dc..fe5a2056e2 100644 --- a/package/expo-package/stream-chat-expo.podspec +++ b/package/expo-package/stream-chat-expo.podspec @@ -12,6 +12,11 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "./ios", :tag => "#{s.version}" } + s.prepare_command = <<-CMD + if [ -d ../shared-native/ios ]; then + cp -f ../shared-native/ios/* ios/ + fi + CMD s.source_files = "ios/**/*.{h,m,mm,swift}" s.private_header_files = "ios/**/*.h" diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec index 698d90f4d5..d440a73484 100644 --- a/package/native-package/stream-chat-react-native.podspec +++ b/package/native-package/stream-chat-react-native.podspec @@ -12,6 +12,11 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "./ios", :tag => "#{s.version}" } + s.prepare_command = <<-CMD + if [ -d ../shared-native/ios ]; then + cp -f ../shared-native/ios/* ios/ + fi + CMD s.source_files = "ios/**/*.{h,m,mm,swift}" s.private_header_files = "ios/**/*.h" From 7aea8293155aa5c64fb77b1250070ee850c6b157 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 15:44:12 +0100 Subject: [PATCH 11/40] fix: ios native gradient hue --- .../shared-native/ios/StreamShimmerView.swift | 55 +++++++++++++++---- .../ios/StreamShimmerViewComponentView.mm | 2 +- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 9f2a73da7d..1334c3575f 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -14,7 +14,7 @@ public final class StreamShimmerView: UIView { private let shimmerLayer = CAGradientLayer() private let centerGradientLayer = CAGradientLayer() - private var baseColor: UIColor = .clear + private var baseColor: UIColor = UIColor(white: 1, alpha: 0) private var highlightColor: UIColor = UIColor(white: 1, alpha: centerGradientAlpha) private var gradientColor: UIColor = .white private var gradientWidth: CGFloat = 0 @@ -60,10 +60,13 @@ public final class StreamShimmerView: UIView { private func setupLayers() { isUserInteractionEnabled = false + shimmerLayer.contentsScale = UIScreen.main.scale + shimmerLayer.allowsEdgeAntialiasing = true shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5) shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5) shimmerLayer.locations = [0.0, 0.2, 0.34, 0.44, 0.56, 0.66, 0.8, 1.0] + centerGradientLayer.contentsScale = UIScreen.main.scale centerGradientLayer.startPoint = CGPoint(x: 0, y: 0.5) centerGradientLayer.endPoint = CGPoint(x: 1, y: 0.5) centerGradientLayer.locations = [0.0, 0.5, 1.0] @@ -127,29 +130,57 @@ public final class StreamShimmerView: UIView { return } - if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil { - return - } + stopAnimation() let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) - let animation = CABasicAnimation(keyPath: "position.x") - animation.fromValue = -shimmerWidth / 2 - animation.toValue = bounds.width + shimmerWidth / 2 + let animation = CABasicAnimation(keyPath: "transform.translation.x") + animation.fromValue = 0 + animation.toValue = bounds.width + shimmerWidth animation.duration = Self.shimmerDuration animation.repeatCount = .infinity - animation.isRemovedOnCompletion = false - animation.fillMode = .forwards + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.isRemovedOnCompletion = true shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey) } private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor { + let resolvedColor = color.resolvedColor(with: traitCollection) + var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 - guard color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { - return color + + if resolvedColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor) + } + + guard + let converted = resolvedColor.cgColor.converted( + to: CGColorSpace(name: CGColorSpace.extendedSRGB)!, + intent: .defaultIntent, + options: nil + ), + let components = converted.components + else { + return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor) + } + + switch components.count { + case 2: + return UIColor( + white: components[0], + alpha: components[1] * alphaFactor + ) + case 4: + return UIColor( + red: components[0], + green: components[1], + blue: components[2], + alpha: components[3] * alphaFactor + ) + default: + return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor) } - return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor) } } diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm index e0961c4ad9..089f0f665e 100644 --- a/package/shared-native/ios/StreamShimmerViewComponentView.mm +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -66,7 +66,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & { const auto &newProps = *std::static_pointer_cast(props); - UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor clearColor]; + UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor colorWithWhite:1 alpha:0]; UIColor *highlightColor = RCTUIColorFromSharedColor(newProps.highlightColor) ?: [UIColor colorWithWhite:1 alpha:0.35]; UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor]; From 6e3f1f5646f06974a9d57494b6da81f172f9e2e4 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 16:06:13 +0100 Subject: [PATCH 12/40] fix: unmount crash on ios shimmer view --- .../shared-native/ios/StreamShimmerViewComponentView.mm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm index 089f0f665e..04221c31b1 100644 --- a/package/shared-native/ios/StreamShimmerViewComponentView.mm +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -49,8 +49,8 @@ - (instancetype)initWithFrame:(CGRect)frame _props = defaultProps; _shimmerView = [[StreamShimmerView alloc] initWithFrame:self.bounds]; - _shimmerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self insertSubview:_shimmerView atIndex:0]; + _shimmerView.userInteractionEnabled = NO; + [self.layer insertSublayer:_shimmerView.layer atIndex:0]; } return self; @@ -59,7 +59,8 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)layoutSubviews { [super layoutSubviews]; - [self sendSubviewToBack:_shimmerView]; + _shimmerView.frame = self.bounds; + [self.layer insertSublayer:_shimmerView.layer atIndex:0]; } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps From 89487306cae11683a7e7d98853f72aa7f49ace70 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 16:07:43 +0100 Subject: [PATCH 13/40] fix: add android files to gitignore too --- package/expo-package/.gitignore | 2 ++ package/native-package/.gitignore | 2 ++ 2 files changed, 4 insertions(+) diff --git a/package/expo-package/.gitignore b/package/expo-package/.gitignore index 6cba831d87..f298bacb4b 100644 --- a/package/expo-package/.gitignore +++ b/package/expo-package/.gitignore @@ -1,5 +1,7 @@ # android android/build +android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt # ios ios/build diff --git a/package/native-package/.gitignore b/package/native-package/.gitignore index 6cba831d87..f298bacb4b 100644 --- a/package/native-package/.gitignore +++ b/package/native-package/.gitignore @@ -1,5 +1,7 @@ # android android/build +android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt # ios ios/build From 89162ee8103f5d767094edebe03a36281a7d0b2d Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 16:18:42 +0100 Subject: [PATCH 14/40] perf: optimize animation restarts and reconstruction of animations when view's detached --- .../shared-native/ios/StreamShimmerView.swift | 27 ++++++++++++++++++- .../ios/StreamShimmerViewComponentView.mm | 15 ++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 1334c3575f..406a630377 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -20,6 +20,7 @@ public final class StreamShimmerView: UIView { private var gradientWidth: CGFloat = 0 private var gradientHeight: CGFloat = 0 private var enabled = false + private var lastAnimatedSize: CGSize = .zero public override init(frame: CGRect) { super.init(frame: frame) @@ -36,6 +37,24 @@ public final class StreamShimmerView: UIView { updateLayersForCurrentState() } + public override func didMoveToWindow() { + super.didMoveToWindow() + if window == nil { + stopAnimation() + } else { + updateLayersForCurrentState() + } + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if let previousTraitCollection, + traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) + { + updateLayersForCurrentState() + } + } + public func apply( baseColor: UIColor, highlightColor: UIColor, @@ -55,6 +74,7 @@ public final class StreamShimmerView: UIView { public func stopAnimation() { shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey) + lastAnimatedSize = .zero } private func setupLayers() { @@ -125,11 +145,15 @@ public final class StreamShimmerView: UIView { } private func updateShimmerAnimation(for bounds: CGRect) { - guard enabled, bounds.width > 0, bounds.height > 0 else { + guard enabled, window != nil, bounds.width > 0, bounds.height > 0 else { stopAnimation() return } + if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil, lastAnimatedSize == bounds.size { + return + } + stopAnimation() let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) @@ -141,6 +165,7 @@ public final class StreamShimmerView: UIView { animation.timingFunction = CAMediaTimingFunction(name: .linear) animation.isRemovedOnCompletion = true shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey) + lastAnimatedSize = bounds.size } private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor { diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm index 04221c31b1..e27b36d9b4 100644 --- a/package/shared-native/ios/StreamShimmerViewComponentView.mm +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -60,7 +60,15 @@ - (void)layoutSubviews { [super layoutSubviews]; _shimmerView.frame = self.bounds; - [self.layer insertSublayer:_shimmerView.layer atIndex:0]; + + BOOL needsReinsert = _shimmerView.layer.superlayer != self.layer; + if (!needsReinsert) { + CALayer *firstLayer = self.layer.sublayers.firstObject; + needsReinsert = firstLayer != _shimmerView.layer; + } + if (needsReinsert) { + [self.layer insertSublayer:_shimmerView.layer atIndex:0]; + } } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps @@ -88,6 +96,11 @@ - (void)prepareForRecycle [_shimmerView stopAnimation]; } +- (void)dealloc +{ + [_shimmerView stopAnimation]; +} + @end #endif From 5e821a002af12ceb51387291f02500197627ca74 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 16:34:15 +0100 Subject: [PATCH 15/40] perf: same optimizations on android --- package/expo-package/android/build.gradle | 2 - package/native-package/android/build.gradle | 2 - .../StreamShimmerFrameLayout.kt | 100 +++++++++++++----- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index 263fe36335..c264227394 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -75,7 +75,6 @@ android { sourceSets { main { - java.srcDirs += [generatedShimmerSourceDir.absolutePath] if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", @@ -119,7 +118,6 @@ tasks.matching { it.name == "preBuild" }.configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { dependsOn("syncSharedShimmerSources") - source(generatedShimmerSourceDir.absolutePath) } repositories { diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index d71ac1d249..402acbd4fa 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -85,7 +85,6 @@ android { sourceSets { main { - java.srcDirs += [generatedShimmerSourceDir.absolutePath] if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", @@ -132,7 +131,6 @@ tasks.matching { it.name == "preBuild" }.configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { dependsOn("syncSharedShimmerSources") - source(generatedShimmerSourceDir.absolutePath) } repositories { diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 00e28a5c22..45f2f932b1 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -9,6 +9,7 @@ import android.graphics.Matrix import android.graphics.Paint import android.graphics.Shader import android.util.AttributeSet +import android.view.View import android.view.animation.LinearInterpolator import android.widget.FrameLayout import kotlin.math.roundToInt @@ -30,7 +31,13 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( private val shimmerMatrix = Matrix() private var shimmerShader: LinearGradient? = null + private var centerGradientShader: LinearGradient? = null private var shimmerTranslateX: Float = 0f + private var centerGradientLeft: Float = 0f + private var centerGradientTop: Float = 0f + private var centerGradientRight: Float = 0f + private var centerGradientBottom: Float = 0f + private var animatedViewWidth: Float = 0f private var animator: ValueAnimator? = null init { @@ -54,18 +61,21 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( fun setGradientColor(color: Int) { if (gradientColor == color) return gradientColor = color + rebuildCenterGradientShader() invalidate() } fun setGradientWidth(widthPx: Float) { if (gradientWidth == widthPx) return gradientWidth = widthPx + rebuildCenterGradientShader() invalidate() } fun setGradientHeight(heightPx: Float) { if (gradientHeight == heightPx) return gradientHeight = heightPx + rebuildCenterGradientShader() invalidate() } @@ -77,7 +87,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } fun updateAnimatorState() { - if (enabled && isAttachedToWindow && width > 0) { + if (shouldAnimateShimmer()) { startShimmer() } else { stopShimmer() @@ -97,9 +107,22 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) rebuildShimmerShader() + rebuildCenterGradientShader() updateAnimatorState() } + override fun onWindowVisibilityChanged(visibility: Int) { + super.onWindowVisibilityChanged(visibility) + updateAnimatorState() + } + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + if (changedView === this) { + updateAnimatorState() + } + } + override fun dispatchDraw(canvas: Canvas) { val viewWidth = width.toFloat() val viewHeight = height.toFloat() @@ -130,27 +153,15 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } private fun drawGradient(canvas: Canvas, viewWidth: Float, viewHeight: Float) { - if (gradientWidth <= 0f || gradientHeight <= 0f) return - - val left = (viewWidth - gradientWidth) / 2f - val top = (viewHeight - gradientHeight) / 2f - val right = left + gradientWidth - val bottom = top + gradientHeight - val gradient = LinearGradient( - left, - top, - right, - top, - intArrayOf( - colorWithAlpha(gradientColor, 0f), - colorWithAlpha(gradientColor, GRADIENT_CENTER_ALPHA), - colorWithAlpha(gradientColor, 0f), - ), - floatArrayOf(0f, 0.5f, 1f), - Shader.TileMode.CLAMP, - ) - gradientPaint.shader = gradient - canvas.drawRect(left, top, right, bottom, gradientPaint) + if (gradientWidth <= 0f || gradientHeight <= 0f || viewWidth <= 0f || viewHeight <= 0f) return + + val shader = centerGradientShader ?: run { + rebuildCenterGradientShader() + centerGradientShader ?: return + } + + gradientPaint.shader = shader + canvas.drawRect(centerGradientLeft, centerGradientTop, centerGradientRight, centerGradientBottom, gradientPaint) gradientPaint.shader = null } @@ -194,12 +205,14 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } private fun startShimmer() { - if (animator != null) return - val viewWidth = width.toFloat() if (viewWidth <= 0f) return + if (animator != null && animatedViewWidth == viewWidth) return + + stopShimmer() val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) + animatedViewWidth = viewWidth animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply { duration = SHIMMER_DURATION_MS repeatCount = ValueAnimator.INFINITE @@ -215,6 +228,45 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( private fun stopShimmer() { animator?.cancel() animator = null + animatedViewWidth = 0f + } + + private fun rebuildCenterGradientShader() { + val viewWidth = width.toFloat() + val viewHeight = height.toFloat() + if (gradientWidth <= 0f || gradientHeight <= 0f || viewWidth <= 0f || viewHeight <= 0f) { + centerGradientShader = null + return + } + + centerGradientLeft = (viewWidth - gradientWidth) / 2f + centerGradientTop = (viewHeight - gradientHeight) / 2f + centerGradientRight = centerGradientLeft + gradientWidth + centerGradientBottom = centerGradientTop + gradientHeight + centerGradientShader = LinearGradient( + centerGradientLeft, + centerGradientTop, + centerGradientRight, + centerGradientTop, + intArrayOf( + colorWithAlpha(gradientColor, 0f), + colorWithAlpha(gradientColor, GRADIENT_CENTER_ALPHA), + colorWithAlpha(gradientColor, 0f), + ), + floatArrayOf(0f, 0.5f, 1f), + Shader.TileMode.CLAMP, + ) + } + + private fun shouldAnimateShimmer(): Boolean { + return enabled && + isAttachedToWindow && + width > 0 && + height > 0 && + visibility == View.VISIBLE && + windowVisibility == View.VISIBLE && + isShown && + alpha > 0f } private fun colorWithAlpha(color: Int, alphaFactor: Float): Int { From 6b3f4a9a8da6868d0ddd77f1190931d54df92b92 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 17:29:38 +0100 Subject: [PATCH 16/40] perf: remove static gradient in favor of fixing smoothing of shimmer --- .../StreamShimmerFrameLayout.kt | 102 +++++------------- 1 file changed, 26 insertions(+), 76 deletions(-) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 45f2f932b1..30cf7ca19e 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -20,23 +20,17 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( ) : FrameLayout(context, attrs) { private var baseColor: Int = DEFAULT_BASE_COLOR private var highlightColor: Int = DEFAULT_HIGHLIGHT_COLOR - private var gradientColor: Int = DEFAULT_GRADIENT_COLOR - private var gradientWidth: Float = 0f - private var gradientHeight: Float = 0f private var enabled: Boolean = true private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } - private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } - private val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + isDither = true + } private val shimmerMatrix = Matrix() private var shimmerShader: LinearGradient? = null - private var centerGradientShader: LinearGradient? = null private var shimmerTranslateX: Float = 0f - private var centerGradientLeft: Float = 0f - private var centerGradientTop: Float = 0f - private var centerGradientRight: Float = 0f - private var centerGradientBottom: Float = 0f private var animatedViewWidth: Float = 0f private var animator: ValueAnimator? = null @@ -59,24 +53,15 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } fun setGradientColor(color: Int) { - if (gradientColor == color) return - gradientColor = color - rebuildCenterGradientShader() - invalidate() + // Intentionally ignored: static center gradient rendering has been removed. } fun setGradientWidth(widthPx: Float) { - if (gradientWidth == widthPx) return - gradientWidth = widthPx - rebuildCenterGradientShader() - invalidate() + // Intentionally ignored: static center gradient rendering has been removed. } fun setGradientHeight(heightPx: Float) { - if (gradientHeight == heightPx) return - gradientHeight = heightPx - rebuildCenterGradientShader() - invalidate() + // Intentionally ignored: static center gradient rendering has been removed. } fun setShimmerEnabled(enabled: Boolean) { @@ -107,7 +92,6 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) rebuildShimmerShader() - rebuildCenterGradientShader() updateAnimatorState() } @@ -135,8 +119,6 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint) drawShimmer(canvas, viewWidth, viewHeight) - drawGradient(canvas, viewWidth, viewHeight) - super.dispatchDraw(canvas) } @@ -152,19 +134,6 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( shimmerPaint.shader = null } - private fun drawGradient(canvas: Canvas, viewWidth: Float, viewHeight: Float) { - if (gradientWidth <= 0f || gradientHeight <= 0f || viewWidth <= 0f || viewHeight <= 0f) return - - val shader = centerGradientShader ?: run { - rebuildCenterGradientShader() - centerGradientShader ?: return - } - - gradientPaint.shader = shader - canvas.drawRect(centerGradientLeft, centerGradientTop, centerGradientRight, centerGradientBottom, gradientPaint) - gradientPaint.shader = null - } - private fun rebuildShimmerShader() { val viewWidth = width.toFloat() if (viewWidth <= 0f) { @@ -173,8 +142,10 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) - val softBase = colorWithAlpha(highlightColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) + val edgeBase = colorWithAlpha(highlightColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) + val softBase = colorWithAlpha(highlightColor, SOFT_HIGHLIGHT_ALPHA_FACTOR) val mediumBase = colorWithAlpha(highlightColor, MID_HIGHLIGHT_ALPHA_FACTOR) + val innerBase = colorWithAlpha(highlightColor, INNER_HIGHLIGHT_ALPHA_FACTOR) shimmerShader = LinearGradient( 0f, 0f, @@ -182,22 +153,28 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( 0f, intArrayOf( baseColor, + edgeBase, softBase, mediumBase, + innerBase, highlightColor, - highlightColor, + innerBase, mediumBase, softBase, + edgeBase, baseColor, ), floatArrayOf( 0f, + 0.08f, 0.2f, - 0.34f, - 0.44f, - 0.56f, - 0.66f, + 0.32f, + 0.4f, + 0.5f, + 0.6f, + 0.68f, 0.8f, + 0.92f, 1f, ), Shader.TileMode.CLAMP, @@ -231,33 +208,6 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( animatedViewWidth = 0f } - private fun rebuildCenterGradientShader() { - val viewWidth = width.toFloat() - val viewHeight = height.toFloat() - if (gradientWidth <= 0f || gradientHeight <= 0f || viewWidth <= 0f || viewHeight <= 0f) { - centerGradientShader = null - return - } - - centerGradientLeft = (viewWidth - gradientWidth) / 2f - centerGradientTop = (viewHeight - gradientHeight) / 2f - centerGradientRight = centerGradientLeft + gradientWidth - centerGradientBottom = centerGradientTop + gradientHeight - centerGradientShader = LinearGradient( - centerGradientLeft, - centerGradientTop, - centerGradientRight, - centerGradientTop, - intArrayOf( - colorWithAlpha(gradientColor, 0f), - colorWithAlpha(gradientColor, GRADIENT_CENTER_ALPHA), - colorWithAlpha(gradientColor, 0f), - ), - floatArrayOf(0f, 0.5f, 1f), - Shader.TileMode.CLAMP, - ) - } - private fun shouldAnimateShimmer(): Boolean { return enabled && isAttachedToWindow && @@ -277,11 +227,11 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( companion object { private const val DEFAULT_BASE_COLOR = 0x00FFFFFF private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF - private const val DEFAULT_GRADIENT_COLOR = Color.WHITE private const val SHIMMER_DURATION_MS = 1200L - private const val SHIMMER_STRIP_WIDTH_RATIO = 1.1f - private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.45f - private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.75f - private const val GRADIENT_CENTER_ALPHA = 0.35f + private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f + private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.16f + private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.32f + private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.55f + private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.78f } } From b8d726e40ab841c44680a613fbaaa365dc1c232b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 17:48:42 +0100 Subject: [PATCH 17/40] fix: improve shimmer visually --- .../StreamShimmerFrameLayout.kt | 13 +++-- .../shared-native/ios/StreamShimmerView.swift | 58 ++++++------------- 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 30cf7ca19e..66eb1c8bde 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -142,6 +142,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) + val transparentHighlight = colorWithAlpha(highlightColor, 0f) val edgeBase = colorWithAlpha(highlightColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) val softBase = colorWithAlpha(highlightColor, SOFT_HIGHLIGHT_ALPHA_FACTOR) val mediumBase = colorWithAlpha(highlightColor, MID_HIGHLIGHT_ALPHA_FACTOR) @@ -152,7 +153,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( shimmerWidth, 0f, intArrayOf( - baseColor, + transparentHighlight, edgeBase, softBase, mediumBase, @@ -162,7 +163,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( mediumBase, softBase, edgeBase, - baseColor, + transparentHighlight, ), floatArrayOf( 0f, @@ -229,9 +230,9 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF private const val SHIMMER_DURATION_MS = 1200L private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f - private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.16f - private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.32f - private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.55f - private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.78f + private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f + private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f + private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.48f + private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.72f } } diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 406a630377..581826e1f5 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -3,22 +3,20 @@ import UIKit @objcMembers public final class StreamShimmerView: UIView { - private static let edgeHighlightAlpha: CGFloat = 0.45 - private static let midHighlightAlpha: CGFloat = 0.75 - private static let centerGradientAlpha: CGFloat = 0.35 - private static let shimmerStripWidthRatio: CGFloat = 1.1 + private static let edgeHighlightAlpha: CGFloat = 0.1 + private static let softHighlightAlpha: CGFloat = 0.24 + private static let midHighlightAlpha: CGFloat = 0.48 + private static let innerHighlightAlpha: CGFloat = 0.72 + private static let defaultHighlightAlpha: CGFloat = 0.35 + private static let shimmerStripWidthRatio: CGFloat = 1.25 private static let shimmerDuration: CFTimeInterval = 1.2 private static let shimmerAnimationKey = "stream_shimmer_translate_x" private let baseLayer = CALayer() private let shimmerLayer = CAGradientLayer() - private let centerGradientLayer = CAGradientLayer() private var baseColor: UIColor = UIColor(white: 1, alpha: 0) - private var highlightColor: UIColor = UIColor(white: 1, alpha: centerGradientAlpha) - private var gradientColor: UIColor = .white - private var gradientWidth: CGFloat = 0 - private var gradientHeight: CGFloat = 0 + private var highlightColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha) private var enabled = false private var lastAnimatedSize: CGSize = .zero @@ -65,9 +63,9 @@ public final class StreamShimmerView: UIView { ) { self.baseColor = baseColor self.highlightColor = highlightColor - self.gradientColor = gradientColor - self.gradientWidth = max(gradientWidth, 0) - self.gradientHeight = max(gradientHeight, 0) + _ = gradientColor + _ = gradientWidth + _ = gradientHeight self.enabled = enabled updateLayersForCurrentState() } @@ -84,16 +82,10 @@ public final class StreamShimmerView: UIView { shimmerLayer.allowsEdgeAntialiasing = true shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5) shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5) - shimmerLayer.locations = [0.0, 0.2, 0.34, 0.44, 0.56, 0.66, 0.8, 1.0] - - centerGradientLayer.contentsScale = UIScreen.main.scale - centerGradientLayer.startPoint = CGPoint(x: 0, y: 0.5) - centerGradientLayer.endPoint = CGPoint(x: 1, y: 0.5) - centerGradientLayer.locations = [0.0, 0.5, 1.0] + shimmerLayer.locations = [0.0, 0.08, 0.2, 0.32, 0.4, 0.5, 0.6, 0.68, 0.8, 0.92, 1.0] layer.addSublayer(baseLayer) layer.addSublayer(shimmerLayer) - layer.addSublayer(centerGradientLayer) } private func updateLayersForCurrentState() { @@ -107,43 +99,29 @@ public final class StreamShimmerView: UIView { baseLayer.backgroundColor = baseColor.cgColor updateShimmerLayer(for: bounds) - updateCenterGradientLayer(for: bounds) updateShimmerAnimation(for: bounds) } private func updateShimmerLayer(for bounds: CGRect) { let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) + let transparentHighlight = color(highlightColor, alphaFactor: 0) shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height) shimmerLayer.colors = [ - baseColor.cgColor, + transparentHighlight.cgColor, color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, + color(highlightColor, alphaFactor: Self.softHighlightAlpha).cgColor, color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, + color(highlightColor, alphaFactor: Self.innerHighlightAlpha).cgColor, highlightColor.cgColor, - highlightColor.cgColor, + color(highlightColor, alphaFactor: Self.innerHighlightAlpha).cgColor, color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, + color(highlightColor, alphaFactor: Self.softHighlightAlpha).cgColor, color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, - baseColor.cgColor, + transparentHighlight.cgColor, ] shimmerLayer.isHidden = !enabled } - private func updateCenterGradientLayer(for bounds: CGRect) { - guard gradientWidth > 0, gradientHeight > 0 else { - centerGradientLayer.isHidden = true - return - } - - let left = (bounds.width - gradientWidth) / 2 - let top = (bounds.height - gradientHeight) / 2 - centerGradientLayer.frame = CGRect(x: left, y: top, width: gradientWidth, height: gradientHeight) - centerGradientLayer.colors = [ - color(gradientColor, alphaFactor: 0).cgColor, - color(gradientColor, alphaFactor: Self.centerGradientAlpha).cgColor, - color(gradientColor, alphaFactor: 0).cgColor, - ] - centerGradientLayer.isHidden = false - } - private func updateShimmerAnimation(for bounds: CGRect) { guard enabled, window != nil, bounds.width > 0, bounds.height > 0 else { stopAnimation() From 490146f6ed49c460e8d4d901fb5e1554c870d87f Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:09:15 +0100 Subject: [PATCH 18/40] refactor: remove unnecessary props from native component --- package/expo-package/android/build.gradle | 16 ++++++---- .../StreamShimmerViewNativeComponent.ts | 5 +--- package/native-package/android/build.gradle | 16 ++++++---- .../StreamShimmerViewNativeComponent.ts | 5 +--- .../StreamShimmerFrameLayout.kt | 27 +++++++++-------- .../StreamShimmerViewManager.kt | 16 +--------- .../shared-native/ios/StreamShimmerView.swift | 30 ++++++++----------- .../ios/StreamShimmerViewComponentView.mm | 5 ---- .../Attachment/ImageLoadingIndicator.tsx | 14 ++------- .../NativeShimmerView.android.tsx | 3 -- .../UIComponents/NativeShimmerView.ios.tsx | 5 ---- .../UIComponents/NativeShimmerView.tsx | 3 -- package/src/native.ts | 3 -- 13 files changed, 54 insertions(+), 94 deletions(-) diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index c264227394..7a028a3564 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -90,10 +90,10 @@ tasks.register("syncSharedShimmerSources") { outputs.upToDateWhen { false } doLast { def sourceDir = null - if (hasAllShimmerFiles(localShimmerSourceDir)) { - sourceDir = localShimmerSourceDir - } else if (hasAllShimmerFiles(sharedShimmerSourceDir)) { + if (hasAllShimmerFiles(sharedShimmerSourceDir)) { sourceDir = sharedShimmerSourceDir + } else if (hasAllShimmerFiles(localShimmerSourceDir)) { + sourceDir = localShimmerSourceDir } if (sourceDir == null) { @@ -104,10 +104,16 @@ tasks.register("syncSharedShimmerSources") { } generatedShimmerSourceDir.mkdirs() + localShimmerSourceDir.mkdirs() shimmerSourceFiles.each { filename -> def srcFile = new File(sourceDir, filename) - def dstFile = new File(generatedShimmerSourceDir, filename) - dstFile.bytes = srcFile.bytes + def generatedDstFile = new File(generatedShimmerSourceDir, filename) + generatedDstFile.bytes = srcFile.bytes + + if (sourceDir != localShimmerSourceDir) { + def localDstFile = new File(localShimmerSourceDir, filename) + localDstFile.bytes = srcFile.bytes + } } } } diff --git a/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts index e14e3658c4..8ba18ab123 100644 --- a/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts +++ b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts @@ -1,15 +1,12 @@ import type { ColorValue, HostComponent, ViewProps } from 'react-native'; -import type { Double, WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; +import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; export interface NativeProps extends ViewProps { baseColor?: ColorValue; enabled?: WithDefault; gradientColor?: ColorValue; - gradientHeight?: Double; - gradientWidth?: Double; - highlightColor?: ColorValue; } export default codegenNativeComponent( diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 402acbd4fa..6241322b31 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -103,10 +103,10 @@ tasks.register("syncSharedShimmerSources") { outputs.upToDateWhen { false } doLast { def sourceDir = null - if (hasAllShimmerFiles(localShimmerSourceDir)) { - sourceDir = localShimmerSourceDir - } else if (hasAllShimmerFiles(sharedShimmerSourceDir)) { + if (hasAllShimmerFiles(sharedShimmerSourceDir)) { sourceDir = sharedShimmerSourceDir + } else if (hasAllShimmerFiles(localShimmerSourceDir)) { + sourceDir = localShimmerSourceDir } if (sourceDir == null) { @@ -117,10 +117,16 @@ tasks.register("syncSharedShimmerSources") { } generatedShimmerSourceDir.mkdirs() + localShimmerSourceDir.mkdirs() shimmerSourceFiles.each { filename -> def srcFile = new File(sourceDir, filename) - def dstFile = new File(generatedShimmerSourceDir, filename) - dstFile.bytes = srcFile.bytes + def generatedDstFile = new File(generatedShimmerSourceDir, filename) + generatedDstFile.bytes = srcFile.bytes + + if (sourceDir != localShimmerSourceDir) { + def localDstFile = new File(localShimmerSourceDir, filename) + localDstFile.bytes = srcFile.bytes + } } } } diff --git a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts index e14e3658c4..8ba18ab123 100644 --- a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts +++ b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts @@ -1,15 +1,12 @@ import type { ColorValue, HostComponent, ViewProps } from 'react-native'; -import type { Double, WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; +import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; export interface NativeProps extends ViewProps { baseColor?: ColorValue; enabled?: WithDefault; gradientColor?: ColorValue; - gradientHeight?: Double; - gradientWidth?: Double; - highlightColor?: ColorValue; } export default codegenNativeComponent( diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 66eb1c8bde..91a655128f 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -19,7 +19,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( attrs: AttributeSet? = null, ) : FrameLayout(context, attrs) { private var baseColor: Int = DEFAULT_BASE_COLOR - private var highlightColor: Int = DEFAULT_HIGHLIGHT_COLOR + private var gradientColor: Int = DEFAULT_GRADIENT_COLOR private var enabled: Boolean = true private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } @@ -45,15 +45,16 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( invalidate() } - fun setHighlightColor(color: Int) { - if (highlightColor == color) return - highlightColor = color + fun setGradientColor(color: Int) { + if (gradientColor == color) return + gradientColor = color rebuildShimmerShader() invalidate() } - fun setGradientColor(color: Int) { - // Intentionally ignored: static center gradient rendering has been removed. + fun setHighlightColor(color: Int) { + // Backward-compatible alias while callsites migrate to gradientColor. + setGradientColor(color) } fun setGradientWidth(widthPx: Float) { @@ -142,11 +143,11 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) - val transparentHighlight = colorWithAlpha(highlightColor, 0f) - val edgeBase = colorWithAlpha(highlightColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) - val softBase = colorWithAlpha(highlightColor, SOFT_HIGHLIGHT_ALPHA_FACTOR) - val mediumBase = colorWithAlpha(highlightColor, MID_HIGHLIGHT_ALPHA_FACTOR) - val innerBase = colorWithAlpha(highlightColor, INNER_HIGHLIGHT_ALPHA_FACTOR) + val transparentHighlight = colorWithAlpha(gradientColor, 0f) + val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) + val softBase = colorWithAlpha(gradientColor, SOFT_HIGHLIGHT_ALPHA_FACTOR) + val mediumBase = colorWithAlpha(gradientColor, MID_HIGHLIGHT_ALPHA_FACTOR) + val innerBase = colorWithAlpha(gradientColor, INNER_HIGHLIGHT_ALPHA_FACTOR) shimmerShader = LinearGradient( 0f, 0f, @@ -158,7 +159,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( softBase, mediumBase, innerBase, - highlightColor, + gradientColor, innerBase, mediumBase, softBase, @@ -227,7 +228,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( companion object { private const val DEFAULT_BASE_COLOR = 0x00FFFFFF - private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF + private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF private const val SHIMMER_DURATION_MS = 1200L private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt index 73022d4eb1..9ff60e550f 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt @@ -2,7 +2,6 @@ package com.streamchatreactnative import androidx.annotation.NonNull import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate @@ -52,22 +51,10 @@ class StreamShimmerViewManager : ViewGroupManager(), view.setBaseColor(color ?: DEFAULT_BASE_COLOR) } - override fun setHighlightColor(view: StreamShimmerFrameLayout, color: Int?) { - view.setHighlightColor(color ?: DEFAULT_HIGHLIGHT_COLOR) - } - override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) { view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR) } - override fun setGradientWidth(view: StreamShimmerFrameLayout, widthDp: Double) { - view.setGradientWidth(PixelUtil.toPixelFromDIP(widthDp.toFloat())) - } - - override fun setGradientHeight(view: StreamShimmerFrameLayout, heightDp: Double) { - view.setGradientHeight(PixelUtil.toPixelFromDIP(heightDp.toFloat())) - } - override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) { super.onDropViewInstance(view) view.setShimmerEnabled(false) @@ -76,7 +63,6 @@ class StreamShimmerViewManager : ViewGroupManager(), companion object { const val REACT_CLASS = "StreamShimmerView" private const val DEFAULT_BASE_COLOR = 0x00FFFFFF - private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF - private const val DEFAULT_GRADIENT_COLOR = 0xFFFFFFFF.toInt() + private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF } } diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 581826e1f5..4bee5b531d 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -16,7 +16,7 @@ public final class StreamShimmerView: UIView { private let shimmerLayer = CAGradientLayer() private var baseColor: UIColor = UIColor(white: 1, alpha: 0) - private var highlightColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha) + private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha) private var enabled = false private var lastAnimatedSize: CGSize = .zero @@ -55,17 +55,11 @@ public final class StreamShimmerView: UIView { public func apply( baseColor: UIColor, - highlightColor: UIColor, gradientColor: UIColor, - gradientWidth: CGFloat, - gradientHeight: CGFloat, enabled: Bool ) { self.baseColor = baseColor - self.highlightColor = highlightColor - _ = gradientColor - _ = gradientWidth - _ = gradientHeight + self.gradientColor = gradientColor self.enabled = enabled updateLayersForCurrentState() } @@ -104,19 +98,19 @@ public final class StreamShimmerView: UIView { private func updateShimmerLayer(for bounds: CGRect) { let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) - let transparentHighlight = color(highlightColor, alphaFactor: 0) + let transparentHighlight = color(gradientColor, alphaFactor: 0) shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height) shimmerLayer.colors = [ transparentHighlight.cgColor, - color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.softHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.innerHighlightAlpha).cgColor, - highlightColor.cgColor, - color(highlightColor, alphaFactor: Self.innerHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.midHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.softHighlightAlpha).cgColor, - color(highlightColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor, + gradientColor.cgColor, + color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor, + color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor, transparentHighlight.cgColor, ] shimmerLayer.isHidden = !enabled diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm index e27b36d9b4..51cc4b4e8b 100644 --- a/package/shared-native/ios/StreamShimmerViewComponentView.mm +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -76,15 +76,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & const auto &newProps = *std::static_pointer_cast(props); UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor colorWithWhite:1 alpha:0]; - UIColor *highlightColor = - RCTUIColorFromSharedColor(newProps.highlightColor) ?: [UIColor colorWithWhite:1 alpha:0.35]; UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor]; [_shimmerView applyWithBaseColor:baseColor - highlightColor:highlightColor gradientColor:gradientColor - gradientWidth:(CGFloat)MAX(newProps.gradientWidth, 0) - gradientHeight:(CGFloat)MAX(newProps.gradientHeight, 0) enabled:newProps.enabled]; [super updateProps:props oldProps:oldProps]; diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index d486d9ac45..d135e7f713 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -1,27 +1,19 @@ import React from 'react'; import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { useTheme } from '../../contexts'; import { NativeShimmerView } from '../UIComponents/NativeShimmerView'; import { ShimmerView } from '../UIComponents/Shimmer/ShimmerView'; export const ImageLoadingIndicator = () => { const { - theme: { - semantics, - shimmer: { width, height }, - }, + theme: { semantics }, } = useTheme(); - if (Platform.OS === 'android' || Platform.OS === 'ios') { return ( diff --git a/package/src/components/UIComponents/NativeShimmerView.android.tsx b/package/src/components/UIComponents/NativeShimmerView.android.tsx index c30334e31c..442d6a691a 100644 --- a/package/src/components/UIComponents/NativeShimmerView.android.tsx +++ b/package/src/components/UIComponents/NativeShimmerView.android.tsx @@ -7,9 +7,6 @@ export type NativeShimmerViewProps = ViewProps & { baseColor?: ColorValue; enabled?: boolean; gradientColor?: ColorValue; - gradientHeight?: number; - gradientWidth?: number; - highlightColor?: ColorValue; }; export const NativeShimmerView = (props: NativeShimmerViewProps) => { diff --git a/package/src/components/UIComponents/NativeShimmerView.ios.tsx b/package/src/components/UIComponents/NativeShimmerView.ios.tsx index 37180d5e84..ea3d56431e 100644 --- a/package/src/components/UIComponents/NativeShimmerView.ios.tsx +++ b/package/src/components/UIComponents/NativeShimmerView.ios.tsx @@ -7,16 +7,11 @@ export type NativeShimmerViewProps = ViewProps & { baseColor?: ColorValue; enabled?: boolean; gradientColor?: ColorValue; - gradientHeight?: number; - gradientWidth?: number; - highlightColor?: ColorValue; }; export const NativeShimmerView = (props: NativeShimmerViewProps) => { const NativeShimmerComponent = NativeHandlers.NativeShimmerView; - console.log('SHIMMER ME: ', NativeShimmerComponent); if (NativeShimmerComponent) { - console.log('SHIMMER ME TIMBERS !'); return ; } diff --git a/package/src/components/UIComponents/NativeShimmerView.tsx b/package/src/components/UIComponents/NativeShimmerView.tsx index 7cf2dd3ac8..ea3d56431e 100644 --- a/package/src/components/UIComponents/NativeShimmerView.tsx +++ b/package/src/components/UIComponents/NativeShimmerView.tsx @@ -7,9 +7,6 @@ export type NativeShimmerViewProps = ViewProps & { baseColor?: ColorValue; enabled?: boolean; gradientColor?: ColorValue; - gradientHeight?: number; - gradientWidth?: number; - highlightColor?: ColorValue; }; export const NativeShimmerView = (props: NativeShimmerViewProps) => { diff --git a/package/src/native.ts b/package/src/native.ts index b871150f2e..856bc817bd 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -297,9 +297,6 @@ export type NativeShimmerViewProps = ViewProps & { baseColor?: ColorValue; enabled?: boolean; gradientColor?: ColorValue; - gradientHeight?: number; - gradientWidth?: number; - highlightColor?: ColorValue; }; type Handlers = { From f217071d75d75512b6451d475cd3fb37a440cd95 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:15:23 +0100 Subject: [PATCH 19/40] chore: remove redundant components --- .../NativeShimmerView.android.tsx | 20 ------------------- .../UIComponents/NativeShimmerView.ios.tsx | 20 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 package/src/components/UIComponents/NativeShimmerView.android.tsx delete mode 100644 package/src/components/UIComponents/NativeShimmerView.ios.tsx diff --git a/package/src/components/UIComponents/NativeShimmerView.android.tsx b/package/src/components/UIComponents/NativeShimmerView.android.tsx deleted file mode 100644 index 442d6a691a..0000000000 --- a/package/src/components/UIComponents/NativeShimmerView.android.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { ColorValue, View, ViewProps } from 'react-native'; - -import { NativeHandlers } from '../../native'; - -export type NativeShimmerViewProps = ViewProps & { - baseColor?: ColorValue; - enabled?: boolean; - gradientColor?: ColorValue; -}; - -export const NativeShimmerView = (props: NativeShimmerViewProps) => { - const NativeShimmerComponent = NativeHandlers.NativeShimmerView; - if (NativeShimmerComponent) { - return ; - } - - const { children, ...rest } = props; - return {children}; -}; diff --git a/package/src/components/UIComponents/NativeShimmerView.ios.tsx b/package/src/components/UIComponents/NativeShimmerView.ios.tsx deleted file mode 100644 index ea3d56431e..0000000000 --- a/package/src/components/UIComponents/NativeShimmerView.ios.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { ColorValue, View, ViewProps } from 'react-native'; - -import { NativeHandlers } from '../../native'; - -export type NativeShimmerViewProps = ViewProps & { - baseColor?: ColorValue; - enabled?: boolean; - gradientColor?: ColorValue; -}; - -export const NativeShimmerView = (props: NativeShimmerViewProps) => { - const NativeShimmerComponent = NativeHandlers.NativeShimmerView; - if (NativeShimmerComponent) { - return ; - } - - const { children, ...rest } = props; - return {children}; -}; From 2139d896043ba4182906e5e0352dc7589bfbe3ae Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:17:38 +0100 Subject: [PATCH 20/40] chore: always use native shimmer view --- .../Attachment/ImageLoadingIndicator.tsx | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index d135e7f713..8b2a3d6dd4 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -1,34 +1,23 @@ import React from 'react'; -import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { useTheme } from '../../contexts'; import { NativeShimmerView } from '../UIComponents/NativeShimmerView'; -import { ShimmerView } from '../UIComponents/Shimmer/ShimmerView'; export const ImageLoadingIndicator = () => { const { theme: { semantics }, } = useTheme(); - if (Platform.OS === 'android' || Platform.OS === 'ios') { - return ( - - - - - - ); - } - return ( - - + + - - + + ); }; From ff83d86c3e6cfec9b854948ff4832e7f636eec66 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:27:56 +0100 Subject: [PATCH 21/40] chore: remove ShimmerContext and provider --- .../MessageList/MessageFlashList.tsx | 127 +++++++-------- .../components/MessageList/MessageList.tsx | 151 +++++++++--------- .../UIComponents/Shimmer/ShimmerContext.tsx | 29 ---- .../UIComponents/Shimmer/ShimmerView.tsx | 78 --------- 4 files changed, 136 insertions(+), 249 deletions(-) delete mode 100644 package/src/components/UIComponents/Shimmer/ShimmerContext.tsx delete mode 100644 package/src/components/UIComponents/Shimmer/ShimmerView.tsx diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 4e2bb64ee2..bcc660427e 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -56,7 +56,6 @@ import { useStableCallback, useStateStore } from '../../hooks'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; -import { ShimmerProvider } from '../UIComponents/Shimmer/ShimmerContext'; let FlashList; @@ -1045,74 +1044,72 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } return ( - - - {processedMessageList.length === 0 && !thread ? ( - - {EmptyStateIndicator ? : null} - - ) : ( - - - - )} - - {messageListLengthAfterUpdate && StickyHeader ? ( - - ) : null} + + {processedMessageList.length === 0 && !thread ? ( + + {EmptyStateIndicator ? : null} - - + - - - {isUnreadNotificationOpen && !threadList ? ( - - - + + )} + + {messageListLengthAfterUpdate && StickyHeader ? ( + ) : null} - + + + + + {isUnreadNotificationOpen && !threadList ? ( + + + + ) : null} + ); }; diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 09a351ef28..7372974f21 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -69,7 +69,6 @@ import { bumpOverlayLayoutRevision } from '../../state-store'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; -import { ShimmerProvider } from '../UIComponents/Shimmer/ShimmerContext'; // This is just to make sure that the scrolling happens in a different task queue. // TODO: Think if we really need this and strive to remove it if we can. @@ -1257,89 +1256,87 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { // TODO: Make sure this is actually overridable as the previous FlatList was. return ( - - - {/* Don't show the empty list indicator for Thread messages */} - {processedMessageList.length === 0 && !thread ? ( - - {EmptyStateIndicator ? : null} - - ) : ( - - + {/* Don't show the empty list indicator for Thread messages */} + {processedMessageList.length === 0 && !thread ? ( + + {EmptyStateIndicator ? : null} + + ) : ( + + - - )} - - {messageListLengthAfterUpdate && StickyHeader ? ( - - ) : null} - - {scrollToBottomButtonVisible ? ( - - - - ) : null} - - - {isUnreadNotificationOpen && !threadList ? ( - - - + maintainVisibleContentPosition={maintainVisibleContentPosition} + maxToRenderPerBatch={30} + onContentSizeChange={onContentSizeChange} + onLayout={onLayout} + onMomentumScrollEnd={onUserScrollEvent} + onScroll={handleScroll} + onScrollBeginDrag={onScrollBeginDrag} + onScrollEndDrag={onScrollEndDrag} + onScrollToIndexFailed={onScrollToIndexFailedRef.current} + onTouchEnd={dismissImagePicker} + onViewableItemsChanged={stableOnViewableItemsChanged} + ref={refCallback} + renderItem={renderItem} + scrollEventThrottle={isLiveStreaming ? 16 : undefined} + showsVerticalScrollIndicator={false} + // @ts-expect-error Safe to do for now + strictMode={isLiveStreaming} + style={flatListStyle} + testID='message-flat-list' + viewabilityConfig={flatListViewabilityConfig} + {...additionalFlatListPropsExcludingStyle} + /> + + )} + + {messageListLengthAfterUpdate && StickyHeader ? ( + ) : null} - + {scrollToBottomButtonVisible ? ( + + + + ) : null} + + + {isUnreadNotificationOpen && !threadList ? ( + + + + ) : null} + ); }; diff --git a/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx b/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx deleted file mode 100644 index ff1d9d8022..0000000000 --- a/package/src/components/UIComponents/Shimmer/ShimmerContext.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'; -import { SharedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; - -type ShimmerContextValue = { - progress: SharedValue; - visibleMessages: SharedValue; -}; - -const ShimmerContext = createContext({} as ShimmerContextValue); - -const SHIMMER_WIDTH = 150; - -type ShimmerProviderProps = PropsWithChildren<{ - visibleMessages: SharedValue; -}>; - -export const ShimmerProvider = ({ children, visibleMessages }: ShimmerProviderProps) => { - const progress = useSharedValue(-SHIMMER_WIDTH); - - useEffect(() => { - progress.value = withRepeat(withTiming(SHIMMER_WIDTH, { duration: 1200 }), -1, true); - }, [progress]); - - const contextValue = useMemo(() => ({ progress, visibleMessages }), [progress, visibleMessages]); - - return {children}; -}; - -export const useShimmerContext = () => useContext(ShimmerContext); diff --git a/package/src/components/UIComponents/Shimmer/ShimmerView.tsx b/package/src/components/UIComponents/Shimmer/ShimmerView.tsx deleted file mode 100644 index b1403932df..0000000000 --- a/package/src/components/UIComponents/Shimmer/ShimmerView.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { PropsWithChildren } from 'react'; -import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; -import Animated, { useAnimatedStyle } from 'react-native-reanimated'; -import Svg, { Rect, Defs, LinearGradient, Stop } from 'react-native-svg'; - -import { useShimmerContext } from './ShimmerContext'; - -import { useMessageContext } from '../../../contexts/messageContext/MessageContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; - -type Props = PropsWithChildren<{ - style?: StyleProp; -}>; - -export const ShimmerView = ({ children, style }: Props) => { - const { progress, visibleMessages } = useShimmerContext(); - const { message } = useMessageContext(); - - const messageId = message?.id; - - const { - theme: { - shimmer: { width, height }, - }, - } = useTheme(); - const styles = useStyles(); - - const animatedStyle = useAnimatedStyle(() => { - return visibleMessages.value.includes(messageId) - ? { - transform: [{ translateX: progress?.value ?? 0 }], - } - : {}; - }, [messageId]); - - return ( - - - - - - - - - - - - - - - - {children} - - ); -}; - -const useStyles = () => { - const { - theme: { - shimmer: { width, height }, - }, - } = useTheme(); - return StyleSheet.create({ - container: { - flex: 1, - overflow: 'hidden', - }, - shimmerContainer: { - width, - height, - }, - content: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - }, - }); -}; From b24481ca687ddc4f94c270cc1cdbdbcf5c117474 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:35:56 +0100 Subject: [PATCH 22/40] chore: cleanup of remnants of old impl --- package/src/components/MessageList/MessageFlashList.tsx | 5 +---- package/src/components/MessageList/MessageList.tsx | 5 +---- package/src/contexts/themeContext/utils/theme.ts | 8 -------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index bcc660427e..0de8b3e356 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -8,7 +8,7 @@ import { ViewToken, } from 'react-native'; -import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated'; +import Animated, { LinearTransition } from 'react-native-reanimated'; import { FlashListProps, FlashListRef, useFlashListContext } from '@shopify/flash-list'; import type { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat'; @@ -700,8 +700,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } }); - const visibleMessages = useSharedValue([]); - /** * FlatList doesn't accept changeable function for onViewableItemsChanged prop. * Thus useRef. @@ -711,7 +709,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => }: { viewableItems: ViewToken[] | undefined; }) => { - visibleMessages.value = viewableItems?.map((viewToken) => viewToken.item.message.id) ?? []; if (!viewableItems) { return; } diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 7372974f21..4766bc8564 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -11,7 +11,7 @@ import { ViewToken, } from 'react-native'; -import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated'; +import Animated, { LinearTransition } from 'react-native-reanimated'; import debounce from 'lodash/debounce'; @@ -579,8 +579,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { } }); - const visibleMessages = useSharedValue([]); - /** * FlatList doesn't accept changeable function for onViewableItemsChanged prop. * Thus useRef. @@ -590,7 +588,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { }: { viewableItems: ViewToken[] | undefined; }) => { - visibleMessages.value = viewableItems?.map((viewToken) => viewToken.item.message.id) ?? []; viewabilityChangedCallback({ inverted, viewableItems }); if (!viewableItems) { diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index df4e447f31..171694ef11 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -991,10 +991,6 @@ export type Theme = { waveform: ViewStyle; }; semantics: typeof lightSemantics; // themed semantics have the same type - shimmer: { - width: number; - height: number; - }; }; export const defaultTheme: Theme = { @@ -1873,8 +1869,4 @@ export const defaultTheme: Theme = { thumb: {}, waveform: {}, }, - shimmer: { - width: 300, - height: 192, - }, }; From 55cf82e5ea91d35b0a9c7e8bf21450fccc7170c2 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:51:38 +0100 Subject: [PATCH 23/40] fix: shimmer animation stopping forever when app enters background --- .../shared-native/ios/StreamShimmerView.swift | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 4bee5b531d..28de90ab51 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -19,15 +19,22 @@ public final class StreamShimmerView: UIView { private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha) private var enabled = false private var lastAnimatedSize: CGSize = .zero + private var isAppActive = true public override init(frame: CGRect) { super.init(frame: frame) setupLayers() + setupLifecycleObservers() } public required init?(coder: NSCoder) { super.init(coder: coder) setupLayers() + setupLifecycleObservers() + } + + deinit { + NotificationCenter.default.removeObserver(self) } public override func layoutSubviews() { @@ -82,6 +89,33 @@ public final class StreamShimmerView: UIView { layer.addSublayer(shimmerLayer) } + private func setupLifecycleObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + @objc + private func handleWillEnterForeground() { + isAppActive = true + updateLayersForCurrentState() + } + + @objc + private func handleDidEnterBackground() { + isAppActive = false + stopAnimation() + } + private func updateLayersForCurrentState() { let bounds = self.bounds guard !bounds.isEmpty else { @@ -117,7 +151,7 @@ public final class StreamShimmerView: UIView { } private func updateShimmerAnimation(for bounds: CGRect) { - guard enabled, window != nil, bounds.width > 0, bounds.height > 0 else { + guard enabled, isAppActive, window != nil, bounds.width > 0, bounds.height > 0 else { stopAnimation() return } From 8c95096241cd394d9f7bdefb44c198f284c07cc7 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 18:52:47 +0100 Subject: [PATCH 24/40] chore: remove logs --- package/src/native.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/package/src/native.ts b/package/src/native.ts index 856bc817bd..ec60c149ef 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -432,9 +432,7 @@ export const registerNativeHandlers = (handlers: Handlers) => { NativeHandlers.Sound = handlers.Sound; } - console.log('CHECK SHIMMER', handlers.NativeShimmerView); if (handlers.NativeShimmerView !== undefined) { - console.log('SHIMMER HERE ?!'); NativeHandlers.NativeShimmerView = handlers.NativeShimmerView; } From 0bef74f6a718a2c9be6965e57e1c54b7e6064ba3 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 7 Mar 2026 19:06:01 +0100 Subject: [PATCH 25/40] fix: failing tests --- .../Attachment/ImageLoadingIndicator.tsx | 2 + .../Attachment/__tests__/Gallery.test.js | 20 +- .../Attachment/__tests__/Giphy.test.js | 13 +- .../MessageTextContainer.test.tsx.snap | 1 + .../__snapshots__/Thread.test.js.snap | 184 +++++++----------- 5 files changed, 89 insertions(+), 131 deletions(-) diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx index 8b2a3d6dd4..7bb2534f1d 100644 --- a/package/src/components/Attachment/ImageLoadingIndicator.tsx +++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx @@ -10,6 +10,8 @@ export const ImageLoadingIndicator = () => { } = useTheme(); return ( { expect(screen.queryAllByTestId('gallery-container').length).toBe(1); }); - fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadStart'); - expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); + fireEvent(screen.getByLabelText('Gallery Image'), 'loadStart'); + await waitFor(() => { + expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); + }); - fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadFinish'); - waitForElementToBeRemoved(() => screen.getByLabelText('Image Loading Indicator')); + fireEvent(screen.getByLabelText('Gallery Image'), 'loadEnd'); + await waitFor(() => { + expect(screen.queryByLabelText('Image Loading Indicator')).toBeNull(); + }); expect(screen.getByLabelText('Gallery Image')).toBeTruthy(); }); }); diff --git a/package/src/components/Attachment/__tests__/Giphy.test.js b/package/src/components/Attachment/__tests__/Giphy.test.js index 0cc42b819a..94d3085926 100644 --- a/package/src/components/Attachment/__tests__/Giphy.test.js +++ b/package/src/components/Attachment/__tests__/Giphy.test.js @@ -8,7 +8,6 @@ import { screen, userEvent, waitFor, - waitForElementToBeRemoved, } from '@testing-library/react-native'; import { MessageProvider } from '../../../contexts/messageContext/MessageContext'; @@ -335,12 +334,8 @@ describe('Giphy', () => { , ); - await waitFor(() => { - expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); - }); - act(() => { - fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'onLoadStart'); + fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'loadStart'); }); await waitFor(() => { @@ -348,10 +343,12 @@ describe('Giphy', () => { }); act(() => { - fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'onLoad'); + fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'loadEnd'); }); - waitForElementToBeRemoved(() => screen.getByLabelText('Image Loading Indicator')); + await waitFor(() => { + expect(screen.queryByLabelText('Image Loading Indicator')).toBeNull(); + }); await waitFor(() => { expect(screen.getByLabelText('Giphy Attachment Image')).toBeTruthy(); diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap index 95f08ee3e1..451f0b4af2 100644 --- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap +++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap @@ -6,6 +6,7 @@ exports[`MessageTextContainer should render message text container 1`] = ` [ { "maxWidth": 256, + "paddingHorizontal": 12, }, {}, undefined, diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index be650c07d5..debfc46e97 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -559,61 +559,51 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "maxWidth": 256, "paddingHorizontal": 12, }, {}, + undefined, ] } + testID="message-text-container" > - - - Message6 - + Message6 - + @@ -899,61 +889,51 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "maxWidth": 256, "paddingHorizontal": 12, }, {}, + undefined, ] } + testID="message-text-container" > - - - Message5 - + Message5 - + @@ -1272,61 +1252,51 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "maxWidth": 256, "paddingHorizontal": 12, }, {}, + undefined, ] } + testID="message-text-container" > - - - Message4 - + Message4 - + @@ -1603,61 +1573,51 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "maxWidth": 256, "paddingHorizontal": 12, }, {}, + undefined, ] } + testID="message-text-container" > - - - Message3 - + Message3 - + From 5441ae2b118a00a1892257c32512bb427cbaee48 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 09:44:50 +0100 Subject: [PATCH 26/40] chore: add minimalistic docs on android --- .../streamchatreactnative/StreamShimmerFrameLayout.kt | 7 +++++++ .../streamchatreactnative/StreamShimmerViewManager.kt | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 91a655128f..159f862078 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -73,6 +73,8 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } fun updateAnimatorState() { + // Centralized lifecycle gate for animation start/stop. This keeps shimmer off for detached or + // hidden views to avoid wasting UI-thread work in long lists. if (shouldAnimateShimmer()) { startShimmer() } else { @@ -142,6 +144,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( return } + // Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look. val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) val transparentHighlight = colorWithAlpha(gradientColor, 0f) val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR) @@ -190,6 +193,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( stopShimmer() + // Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly. val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f) animatedViewWidth = viewWidth animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply { @@ -211,6 +215,8 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } private fun shouldAnimateShimmer(): Boolean { + // `isShown` and explicit visibility/window checks cover different hide paths in nested + // hierarchies. Keeping them all prevents animations running when not visible to the user. return enabled && isAttachedToWindow && width > 0 && @@ -222,6 +228,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } private fun colorWithAlpha(color: Int, alphaFactor: Float): Int { + // Preserve RGB while shaping only alpha; used for symmetric highlight falloff in gradient stops. val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255) return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) } diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt index 9ff60e550f..2b3e587a07 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt @@ -7,6 +7,12 @@ import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface +/** + * Fabric manager for StreamShimmerView. + * + * This must be a real ViewGroupManager because shimmer wraps React children. Using a non-group + * manager here causes runtime cast failures in Fabric mounting paths. + */ class StreamShimmerViewManager : ViewGroupManager(), StreamShimmerViewManagerInterface { private val delegate = StreamShimmerViewManagerDelegate(this) @@ -22,6 +28,8 @@ class StreamShimmerViewManager : ViewGroupManager(), override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) { super.onAfterUpdateTransaction(view) + // Prop batches can change visibility/enabled/colors together, so we re-evaluate the animator once + // after every transaction to keep state consistent and avoid duplicate start/stop churn. view.updateAnimatorState() } @@ -57,6 +65,7 @@ class StreamShimmerViewManager : ViewGroupManager(), override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) { super.onDropViewInstance(view) + // Defensive shutdown for recycled/unmounted views; avoids animator leaks in list-heavy screens. view.setShimmerEnabled(false) } From bd82c3d5e122c753630c0988f579eca69e78786a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 09:47:01 +0100 Subject: [PATCH 27/40] fix: remove redundant setters --- .../StreamShimmerFrameLayout.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 159f862078..55c96ab34e 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -52,19 +52,6 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( invalidate() } - fun setHighlightColor(color: Int) { - // Backward-compatible alias while callsites migrate to gradientColor. - setGradientColor(color) - } - - fun setGradientWidth(widthPx: Float) { - // Intentionally ignored: static center gradient rendering has been removed. - } - - fun setGradientHeight(heightPx: Float) { - // Intentionally ignored: static center gradient rendering has been removed. - } - fun setShimmerEnabled(enabled: Boolean) { if (this.enabled == enabled) return this.enabled = enabled From 118c114e4c4aa949240682d55f340c8dcf546b28 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 10:08:02 +0100 Subject: [PATCH 28/40] chore: extend android docs a bit --- .../streamchatreactnative/StreamShimmerFrameLayout.kt | 11 +++++++++++ .../streamchatreactnative/StreamShimmerViewManager.kt | 8 ++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 55c96ab34e..4fe0d14420 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -14,6 +14,15 @@ import android.view.animation.LinearInterpolator import android.widget.FrameLayout import kotlin.math.roundToInt +/** + * Native shimmer container used by `StreamShimmerView`. + * + * This view draws a base color plus a moving highlight strip directly on canvas and still behaves + * like a regular container for React children. The animation runs fully on the native side so it + * does not depend on JS-driven frame updates. It automatically stops animating when the view is + * detached or not visible, rebuilds its shader when size or colors change, and waits for valid + * dimensions before starting animation to avoid invalid draw/animation states. + */ class StreamShimmerFrameLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -125,6 +134,8 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( } private fun rebuildShimmerShader() { + // Recreates the shimmer gradient for the current width/colors. This allocates shader state, + // so keep calls tied to real changes (size or color updates), not per frame execution. val viewWidth = width.toFloat() if (viewWidth <= 0f) { shimmerShader = null diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt index 2b3e587a07..3110055853 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt @@ -10,8 +10,12 @@ import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface /** * Fabric manager for StreamShimmerView. * - * This must be a real ViewGroupManager because shimmer wraps React children. Using a non-group - * manager here causes runtime cast failures in Fabric mounting paths. + * It creates the native shimmer layout, maps React props to native setters, and exposes child + * management methods so Fabric can mount and unmount children correctly inside this container. + * The manager rechecks animation state after prop transactions and disables shimmer when a view + * instance is dropped as a defensive cleanup step for recycled or unmounted views. Because the + * shimmer view wraps React children, this must remain a real ViewGroupManager as using a non-group + * manager can fail in Fabric mounting paths at runtime. */ class StreamShimmerViewManager : ViewGroupManager(), StreamShimmerViewManagerInterface { From a61661105826e225568bcf3e550bbd12e7dccd19 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 10:19:01 +0100 Subject: [PATCH 29/40] chore: add docs for ios and fix some for android --- .../StreamShimmerFrameLayout.kt | 4 ++++ .../shared-native/ios/StreamShimmerView.swift | 18 ++++++++++++++++++ .../ios/StreamShimmerViewComponentView.mm | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt index 4fe0d14420..a82dcafbbb 100644 --- a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt +++ b/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt @@ -80,10 +80,13 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() + // Reattachment (including reparenting) should recheck visibility state and restart only if + // this instance is eligible to animate. updateAnimatorState() } override fun onDetachedFromWindow() { + // Detached views are not drawable; stop and clear animator so a future attach starts cleanly. stopShimmer() super.onDetachedFromWindow() } @@ -187,6 +190,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor( private fun startShimmer() { val viewWidth = width.toFloat() if (viewWidth <= 0f) return + // Keep the existing animator if the same-sized shimmer is already active. if (animator != null && animatedViewWidth == viewWidth) return stopShimmer() diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift index 28de90ab51..f77946d9a9 100644 --- a/package/shared-native/ios/StreamShimmerView.swift +++ b/package/shared-native/ios/StreamShimmerView.swift @@ -1,6 +1,11 @@ import QuartzCore import UIKit +/// Native shimmer view used by the Fabric component view. +/// +/// It renders a base layer and a moving gradient highlight entirely in native code, so shimmer +/// animation stays off the JS thread. The view updates its gradient when size or colors change and +/// stops animation when it is not drawable (backgrounded, detached, hidden, or zero sized). @objcMembers public final class StreamShimmerView: UIView { private static let edgeHighlightAlpha: CGFloat = 0.1 @@ -45,8 +50,12 @@ public final class StreamShimmerView: UIView { public override func didMoveToWindow() { super.didMoveToWindow() if window == nil { + // Detaching from window means this view is no longer drawable. Stop and clear animation so + // a later reattach starts from a clean state. stopAnimation() } else { + // Reattaching (including reparenting across windows) re-evaluates state and restarts only + // when needed by current bounds/visibility/enablement. updateLayersForCurrentState() } } @@ -56,6 +65,8 @@ public final class StreamShimmerView: UIView { if let previousTraitCollection, traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + // In current usage, colors are typically driven by JS props. We still refresh on trait + // changes so dynamically resolved native colors remain correct if that path is used later. updateLayersForCurrentState() } } @@ -106,6 +117,8 @@ public final class StreamShimmerView: UIView { @objc private func handleWillEnterForeground() { + // iOS can drop active layer animations while the app is backgrounded. We explicitly rerun + // a state update on foreground so shimmer reliably restarts when returning to the app. isAppActive = true updateLayersForCurrentState() } @@ -131,6 +144,8 @@ public final class StreamShimmerView: UIView { } private func updateShimmerLayer(for bounds: CGRect) { + // Rebuild the shimmer gradient for current width/colors. Keep this tied to real state changes + // such as layout/prop updates, not continuous per frame calls. let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) let transparentHighlight = color(gradientColor, alphaFactor: 0) shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height) @@ -156,12 +171,14 @@ public final class StreamShimmerView: UIView { return } + // If an animation already exists for the same size, keep it running instead of restarting. if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil, lastAnimatedSize == bounds.size { return } stopAnimation() + // Start just outside the left edge and sweep fully past the right edge for a clean pass. let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1) let animation = CABasicAnimation(keyPath: "transform.translation.x") animation.fromValue = 0 @@ -175,6 +192,7 @@ public final class StreamShimmerView: UIView { } private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor { + // Preserve the resolved color channels and shape only alpha for smooth highlight falloff. let resolvedColor = color.resolvedColor(with: traitCollection) var red: CGFloat = 0 diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm index 51cc4b4e8b..c6161d18a8 100644 --- a/package/shared-native/ios/StreamShimmerViewComponentView.mm +++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm @@ -33,6 +33,9 @@ @interface StreamShimmerViewComponentView () @end +// Fabric bridge for StreamShimmerView. This component view owns the native shimmer instance, +// applies codegen props, and keeps shimmer rendered as a background layer while Fabric manages +// React children. Keeping shimmer as a layer avoids child-order conflicts during mount/unmount. @implementation StreamShimmerViewComponentView { StreamShimmerView *_shimmerView; } @@ -61,6 +64,8 @@ - (void)layoutSubviews [super layoutSubviews]; _shimmerView.frame = self.bounds; + // Keep shimmer pinned as the layer furthest back. Some layer operations can reorder sublayers, and + // this guard restores expected layering without touching Fabric managed child views. BOOL needsReinsert = _shimmerView.layer.superlayer != self.layer; if (!needsReinsert) { CALayer *firstLayer = self.layer.sublayers.firstObject; @@ -88,6 +93,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & - (void)prepareForRecycle { [super prepareForRecycle]; + // Defensive cleanup for recycled cells/views so offscreen instances do not keep animating. [_shimmerView stopAnimation]; } From f363981791289eb30f2fac4bb4659522ddb2f9f2 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 17:24:23 +0100 Subject: [PATCH 30/40] feat: introduce file syncing so that debugging is easy --- examples/ExpoMessaging/package.json | 4 + examples/SampleApp/ios/Podfile.lock | 2 +- examples/SampleApp/package.json | 2 +- package/expo-package/android/build.gradle | 1 - package/expo-package/stream-chat-expo.podspec | 7 +- package/native-package/android/build.gradle | 1 - .../stream-chat-react-native.podspec | 7 +- package/package.json | 2 + package/scripts/sync-shimmer-from-package.sh | 96 +++++++++++++++++++ package/scripts/sync-shimmer-native.sh | 26 ++++- package/src/components/Attachment/Gallery.tsx | 18 ++-- 11 files changed, 146 insertions(+), 20 deletions(-) create mode 100755 package/scripts/sync-shimmer-from-package.sh diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 05cdda1247..2dc1ac6f74 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -3,6 +3,10 @@ "version": "1.0.0", "main": "expo-router/entry", "scripts": { + "sync-native": "bash ../../package/scripts/sync-shimmer-from-package.sh expo-package && bash ../../package/scripts/sync-shimmer-native.sh expo-package", + "prestart": "yarn sync-native", + "preandroid": "yarn sync-native", + "preios": "yarn sync-native", "start": "expo start --dev-client", "android": "expo run:android", "ios": "expo run:ios", diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index dc6d618e46..8bc78dfc9f 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -3735,7 +3735,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: 4c7b4132ab6b45c016273a35e465583c3cd8b151 + stream-chat-react-native: 7849b617fe4d5f0c5eec30dc69a4f8e4a049d1a3 Teleport: 9ae328b3384204b23236e47bd0d8e994ce6bdb48 Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 42cba74738..66893ae7ba 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/GetStream/stream-chat-react-native.git" }, "scripts": { - "sync-native": "bash ../../package/scripts/sync-shimmer-native.sh native-package", + "sync-native": "bash ../../package/scripts/sync-shimmer-from-package.sh native-package && bash ../../package/scripts/sync-shimmer-native.sh native-package", "preandroid": "yarn sync-native", "android": "react-native run-android", "preios": "yarn sync-native", diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index 7a028a3564..1de56883d6 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -29,7 +29,6 @@ if (isNewArchitectureEnabled()) { def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger() } - def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") diff --git a/package/expo-package/stream-chat-expo.podspec b/package/expo-package/stream-chat-expo.podspec index fe5a2056e2..8afe4b8765 100644 --- a/package/expo-package/stream-chat-expo.podspec +++ b/package/expo-package/stream-chat-expo.podspec @@ -14,7 +14,12 @@ Pod::Spec.new do |s| s.source = { :git => "./ios", :tag => "#{s.version}" } s.prepare_command = <<-CMD if [ -d ../shared-native/ios ]; then - cp -f ../shared-native/ios/* ios/ + mkdir -p ios + for file in ../shared-native/ios/*; do + dest="ios/$(basename "$file")" + rm -f "$dest" + cp "$file" "$dest" + done fi CMD s.source_files = "ios/**/*.{h,m,mm,swift}" diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 6241322b31..ce2a5a8953 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -36,7 +36,6 @@ def getExtOrDefault(name) { def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger() } - def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec index d440a73484..edef51e88b 100644 --- a/package/native-package/stream-chat-react-native.podspec +++ b/package/native-package/stream-chat-react-native.podspec @@ -14,7 +14,12 @@ Pod::Spec.new do |s| s.source = { :git => "./ios", :tag => "#{s.version}" } s.prepare_command = <<-CMD if [ -d ../shared-native/ios ]; then - cp -f ../shared-native/ios/* ios/ + mkdir -p ios + for file in ../shared-native/ios/*; do + dest="ios/$(basename "$file")" + rm -f "$dest" + cp "$file" "$dest" + done fi CMD s.source_files = "ios/**/*.{h,m,mm,swift}" diff --git a/package/package.json b/package/package.json index ae2aa00e6b..6116733801 100644 --- a/package/package.json +++ b/package/package.json @@ -24,6 +24,8 @@ ], "scripts": { "install-all": "(yarn install --force && (cd native-package && yarn install --force) && (cd expo-package && yarn install --force))", + "shimmer:sync": "bash ./scripts/sync-shimmer-native.sh all", + "shimmer:clean-copies": "bash ./scripts/clean-shimmer-native-copies.sh all", "build": "rimraf lib && yarn run --silent build-translations && bob build && yarn run --silent copy-translations", "build-translations": "i18next-cli sync", "copy-translations": "echo '\u001b[34mℹ\u001b[0m Copying translation files to \u001b[34mlib/typescript/i18n\u001b[0m' && cp -R -f ./src/i18n ./lib/typescript/i18n && echo '\u001b[32m✓\u001b[0m Done Copying Translations'", diff --git a/package/scripts/sync-shimmer-from-package.sh b/package/scripts/sync-shimmer-from-package.sh new file mode 100755 index 0000000000..2fb7089072 --- /dev/null +++ b/package/scripts/sync-shimmer-from-package.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET="${1:-all}" + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" +SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios" + +ANDROID_FILES=( + "StreamShimmerFrameLayout.kt" + "StreamShimmerViewManager.kt" +) +IOS_FILES=( + "StreamShimmerViewComponentView.h" + "StreamShimmerViewComponentView.mm" + "StreamShimmerView.swift" +) + +copy_file() { + local src_file="$1" + local dst_file="$2" + if [ -e "$dst_file" ]; then + local src_inode + local dst_inode + src_inode=$(stat -f %i "$src_file" 2>/dev/null || echo "") + dst_inode=$(stat -f %i "$dst_file" 2>/dev/null || echo "") + if [ -n "$src_inode" ] && [ "$src_inode" = "$dst_inode" ]; then + return + fi + fi + cp "$src_file" "$dst_file" +} + +sync_from_package() { + local package_name="$1" + local android_src_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + local ios_src_dir="$ROOT_DIR/$package_name/ios" + local missing=0 + + mkdir -p "$SHARED_ANDROID_DIR" "$SHARED_IOS_DIR" + + for filename in "${ANDROID_FILES[@]}"; do + local src_file="$android_src_dir/$filename" + local dst_file="$SHARED_ANDROID_DIR/$filename" + if [ ! -f "$src_file" ]; then + echo "Missing Android shimmer source in $package_name (skipping reverse sync): $src_file" + missing=1 + continue + fi + copy_file "$src_file" "$dst_file" + done + + for filename in "${IOS_FILES[@]}"; do + local src_file="$ios_src_dir/$filename" + local dst_file="$SHARED_IOS_DIR/$filename" + if [ ! -f "$src_file" ]; then + echo "Missing iOS shimmer source in $package_name (skipping reverse sync): $src_file" + missing=1 + continue + fi + copy_file "$src_file" "$dst_file" + done + + if [ "$missing" -eq 1 ]; then + return 0 + fi +} + +case "$TARGET" in + native-package) + sync_from_package "native-package" + ;; + expo-package) + sync_from_package "expo-package" + ;; + all) + # Prefer native-package as source if both are present. + if [ -f "$ROOT_DIR/native-package/ios/StreamShimmerView.swift" ]; then + sync_from_package "native-package" + elif [ -f "$ROOT_DIR/expo-package/ios/StreamShimmerView.swift" ]; then + sync_from_package "expo-package" + else + echo "No package shimmer sources found to sync from" + exit 1 + fi + ;; + *) + echo "Unknown target: $TARGET" + echo "Expected one of: native-package, expo-package, all" + exit 1 + ;; +esac + +echo "Synchronized shimmer native files from package mirror to shared-native for target: $TARGET" diff --git a/package/scripts/sync-shimmer-native.sh b/package/scripts/sync-shimmer-native.sh index 6a22b3f071..f915780716 100644 --- a/package/scripts/sync-shimmer-native.sh +++ b/package/scripts/sync-shimmer-native.sh @@ -8,19 +8,35 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" ANDROID_SRC_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" IOS_SRC_DIR="$ROOT_DIR/shared-native/ios" +copy_file() { + local src_file="$1" + local dst_file="$2" + if [ -e "$dst_file" ]; then + local src_inode + local dst_inode + src_inode=$(stat -f %i "$src_file" 2>/dev/null || echo "") + dst_inode=$(stat -f %i "$dst_file" 2>/dev/null || echo "") + if [ -n "$src_inode" ] && [ "$src_inode" = "$dst_inode" ]; then + return + fi + fi + rm -f "$dst_file" + cp "$src_file" "$dst_file" +} + copy_to_package() { local package_name="$1" local android_dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" local ios_dst_dir="$ROOT_DIR/$package_name/ios" mkdir -p "$android_dst_dir" - cp "$ANDROID_SRC_DIR/StreamShimmerFrameLayout.kt" "$android_dst_dir/StreamShimmerFrameLayout.kt" - cp "$ANDROID_SRC_DIR/StreamShimmerViewManager.kt" "$android_dst_dir/StreamShimmerViewManager.kt" + copy_file "$ANDROID_SRC_DIR/StreamShimmerFrameLayout.kt" "$android_dst_dir/StreamShimmerFrameLayout.kt" + copy_file "$ANDROID_SRC_DIR/StreamShimmerViewManager.kt" "$android_dst_dir/StreamShimmerViewManager.kt" mkdir -p "$ios_dst_dir" - cp "$IOS_SRC_DIR/StreamShimmerViewComponentView.h" "$ios_dst_dir/StreamShimmerViewComponentView.h" - cp "$IOS_SRC_DIR/StreamShimmerViewComponentView.mm" "$ios_dst_dir/StreamShimmerViewComponentView.mm" - cp "$IOS_SRC_DIR/StreamShimmerView.swift" "$ios_dst_dir/StreamShimmerView.swift" + copy_file "$IOS_SRC_DIR/StreamShimmerViewComponentView.h" "$ios_dst_dir/StreamShimmerViewComponentView.h" + copy_file "$IOS_SRC_DIR/StreamShimmerViewComponentView.mm" "$ios_dst_dir/StreamShimmerViewComponentView.mm" + copy_file "$IOS_SRC_DIR/StreamShimmerView.swift" "$ios_dst_dir/StreamShimmerView.swift" } case "$TARGET" in diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 9ce0c22066..82ea8e3031 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -444,15 +444,15 @@ const GalleryImageThumbnail = ({ ) : ( <> - - {isLoadingImage ? : null} + {/**/} + {true ? : null} )} From 44e8a635206761c2c5bff2cd3f4c4ea650e4baa7 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 17:59:42 +0100 Subject: [PATCH 31/40] refactor: move native code in dedicated directory --- examples/SampleApp/ios/Podfile.lock | 2 +- package/expo-package/.gitignore | 7 +- package/expo-package/android/build.gradle | 46 +++++----- package/expo-package/stream-chat-expo.podspec | 8 +- package/native-package/.gitignore | 7 +- package/native-package/android/build.gradle | 46 +++++----- .../stream-chat-react-native.podspec | 8 +- .../scripts/clean-shimmer-native-copies.sh | 5 +- package/scripts/sync-shimmer-from-package.sh | 83 +++++++------------ package/scripts/sync-shimmer-native.sh | 53 ++++++------ .../StreamShimmerFrameLayout.kt | 0 .../StreamShimmerViewManager.kt | 0 12 files changed, 112 insertions(+), 153 deletions(-) rename package/shared-native/android/{src/main/java/com/streamchatreactnative => }/StreamShimmerFrameLayout.kt (100%) rename package/shared-native/android/{src/main/java/com/streamchatreactnative => }/StreamShimmerViewManager.kt (100%) diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index 8bc78dfc9f..18b1bdbe28 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -3735,7 +3735,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: 7849b617fe4d5f0c5eec30dc69a4f8e4a049d1a3 + stream-chat-react-native: 19671d89c0503d527f1d5d7ff3f7a5359de7dbd5 Teleport: 9ae328b3384204b23236e47bd0d8e994ce6bdb48 Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed diff --git a/package/expo-package/.gitignore b/package/expo-package/.gitignore index f298bacb4b..81e0235dce 100644 --- a/package/expo-package/.gitignore +++ b/package/expo-package/.gitignore @@ -1,10 +1,7 @@ # android android/build -android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt -android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt +android/src/main/java/com/streamchatreactnative/shared # ios ios/build -ios/StreamShimmerView.swift -ios/StreamShimmerViewComponentView.h -ios/StreamShimmerViewComponentView.mm +ios/shared diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index 1de56883d6..d64a081fba 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -29,12 +29,10 @@ if (isNewArchitectureEnabled()) { def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger() } -def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") -def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") -def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") -def shimmerSourceFiles = ["StreamShimmerFrameLayout.kt", "StreamShimmerViewManager.kt"] -def hasAllShimmerFiles = { File dir -> - shimmerSourceFiles.every { filename -> new File(dir, filename).exists() } +def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared") +def sharedNativeRootDir = new File(projectDir, "../../shared-native/android") +def hasNativeSources = { File dir -> + dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty() } android { @@ -77,6 +75,7 @@ android { if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", + "src/main/java/com/streamchatreactnative/shared", "${project.buildDir}/generated/source/codegen/java" ] } @@ -85,34 +84,31 @@ android { } tasks.register("syncSharedShimmerSources") { - outputs.dir(generatedShimmerSourceDir) + outputs.dir(localSharedNativeRootDir) outputs.upToDateWhen { false } doLast { - def sourceDir = null - if (hasAllShimmerFiles(sharedShimmerSourceDir)) { - sourceDir = sharedShimmerSourceDir - } else if (hasAllShimmerFiles(localShimmerSourceDir)) { - sourceDir = localShimmerSourceDir + def sourceRootDir = null + if (hasNativeSources(localSharedNativeRootDir)) { + sourceRootDir = localSharedNativeRootDir + } else if (hasNativeSources(sharedNativeRootDir)) { + sourceRootDir = sharedNativeRootDir } - if (sourceDir == null) { + if (sourceRootDir == null) { throw new GradleException( - "Missing shimmer native sources. Expected either src/main/java/com/streamchatreactnative/*.kt " + - "or ../../shared-native/android/src/main/java/com/streamchatreactnative/*.kt." + "Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " + + "or ../../shared-native/android/**/*.{kt,java}." ) } - generatedShimmerSourceDir.mkdirs() - localShimmerSourceDir.mkdirs() - shimmerSourceFiles.each { filename -> - def srcFile = new File(sourceDir, filename) - def generatedDstFile = new File(generatedShimmerSourceDir, filename) - generatedDstFile.bytes = srcFile.bytes - - if (sourceDir != localShimmerSourceDir) { - def localDstFile = new File(localShimmerSourceDir, filename) - localDstFile.bytes = srcFile.bytes + if (sourceRootDir != localSharedNativeRootDir) { + project.delete(localSharedNativeRootDir) + project.copy { + from(sourceRootDir) + into(localSharedNativeRootDir) } + } else if (!hasNativeSources(localSharedNativeRootDir)) { + throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.") } } } diff --git a/package/expo-package/stream-chat-expo.podspec b/package/expo-package/stream-chat-expo.podspec index 8afe4b8765..0a9f977668 100644 --- a/package/expo-package/stream-chat-expo.podspec +++ b/package/expo-package/stream-chat-expo.podspec @@ -14,12 +14,8 @@ Pod::Spec.new do |s| s.source = { :git => "./ios", :tag => "#{s.version}" } s.prepare_command = <<-CMD if [ -d ../shared-native/ios ]; then - mkdir -p ios - for file in ../shared-native/ios/*; do - dest="ios/$(basename "$file")" - rm -f "$dest" - cp "$file" "$dest" - done + mkdir -p ios/shared + rsync -a --delete ../shared-native/ios/ ios/shared/ fi CMD s.source_files = "ios/**/*.{h,m,mm,swift}" diff --git a/package/native-package/.gitignore b/package/native-package/.gitignore index f298bacb4b..81e0235dce 100644 --- a/package/native-package/.gitignore +++ b/package/native-package/.gitignore @@ -1,10 +1,7 @@ # android android/build -android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt -android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt +android/src/main/java/com/streamchatreactnative/shared # ios ios/build -ios/StreamShimmerView.swift -ios/StreamShimmerViewComponentView.h -ios/StreamShimmerViewComponentView.mm +ios/shared diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index ce2a5a8953..6113b5c74a 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -36,12 +36,10 @@ def getExtOrDefault(name) { def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger() } -def localShimmerSourceDir = new File(projectDir, "src/main/java/com/streamchatreactnative") -def sharedShimmerSourceDir = new File(projectDir, "../../shared-native/android/src/main/java/com/streamchatreactnative") -def generatedShimmerSourceDir = new File(project.buildDir, "generated/stream-shimmer/src/main/java/com/streamchatreactnative") -def shimmerSourceFiles = ["StreamShimmerFrameLayout.kt", "StreamShimmerViewManager.kt"] -def hasAllShimmerFiles = { File dir -> - shimmerSourceFiles.every { filename -> new File(dir, filename).exists() } +def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared") +def sharedNativeRootDir = new File(projectDir, "../../shared-native/android") +def hasNativeSources = { File dir -> + dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty() } android { @@ -87,6 +85,7 @@ android { if (isNewArchitectureEnabled()) { java.srcDirs += [ "src/newarch", + "src/main/java/com/streamchatreactnative/shared", // This is needed to build Kotlin project with NewArch enabled "${project.buildDir}/generated/source/codegen/java" ] @@ -98,34 +97,31 @@ android { } tasks.register("syncSharedShimmerSources") { - outputs.dir(generatedShimmerSourceDir) + outputs.dir(localSharedNativeRootDir) outputs.upToDateWhen { false } doLast { - def sourceDir = null - if (hasAllShimmerFiles(sharedShimmerSourceDir)) { - sourceDir = sharedShimmerSourceDir - } else if (hasAllShimmerFiles(localShimmerSourceDir)) { - sourceDir = localShimmerSourceDir + def sourceRootDir = null + if (hasNativeSources(localSharedNativeRootDir)) { + sourceRootDir = localSharedNativeRootDir + } else if (hasNativeSources(sharedNativeRootDir)) { + sourceRootDir = sharedNativeRootDir } - if (sourceDir == null) { + if (sourceRootDir == null) { throw new GradleException( - "Missing shimmer native sources. Expected either src/main/java/com/streamchatreactnative/*.kt " + - "or ../../shared-native/android/src/main/java/com/streamchatreactnative/*.kt." + "Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " + + "or ../../shared-native/android/**/*.{kt,java}." ) } - generatedShimmerSourceDir.mkdirs() - localShimmerSourceDir.mkdirs() - shimmerSourceFiles.each { filename -> - def srcFile = new File(sourceDir, filename) - def generatedDstFile = new File(generatedShimmerSourceDir, filename) - generatedDstFile.bytes = srcFile.bytes - - if (sourceDir != localShimmerSourceDir) { - def localDstFile = new File(localShimmerSourceDir, filename) - localDstFile.bytes = srcFile.bytes + if (sourceRootDir != localSharedNativeRootDir) { + project.delete(localSharedNativeRootDir) + project.copy { + from(sourceRootDir) + into(localSharedNativeRootDir) } + } else if (!hasNativeSources(localSharedNativeRootDir)) { + throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.") } } } diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec index edef51e88b..d1c4a58a24 100644 --- a/package/native-package/stream-chat-react-native.podspec +++ b/package/native-package/stream-chat-react-native.podspec @@ -14,12 +14,8 @@ Pod::Spec.new do |s| s.source = { :git => "./ios", :tag => "#{s.version}" } s.prepare_command = <<-CMD if [ -d ../shared-native/ios ]; then - mkdir -p ios - for file in ../shared-native/ios/*; do - dest="ios/$(basename "$file")" - rm -f "$dest" - cp "$file" "$dest" - done + mkdir -p ios/shared + rsync -a --delete ../shared-native/ios/ ios/shared/ fi CMD s.source_files = "ios/**/*.{h,m,mm,swift}" diff --git a/package/scripts/clean-shimmer-native-copies.sh b/package/scripts/clean-shimmer-native-copies.sh index 22b7779a8b..f8cea28908 100644 --- a/package/scripts/clean-shimmer-native-copies.sh +++ b/package/scripts/clean-shimmer-native-copies.sh @@ -8,9 +8,10 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" clean_package() { local package_name="$1" - local android_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" + local android_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared" + local ios_dir="$ROOT_DIR/$package_name/ios/shared" - rm -f "$android_dir/StreamShimmerFrameLayout.kt" "$android_dir/StreamShimmerViewManager.kt" + rm -rf "$android_dir" "$ios_dir" } case "$TARGET" in diff --git a/package/scripts/sync-shimmer-from-package.sh b/package/scripts/sync-shimmer-from-package.sh index 2fb7089072..fa7a28365c 100755 --- a/package/scripts/sync-shimmer-from-package.sh +++ b/package/scripts/sync-shimmer-from-package.sh @@ -5,66 +5,45 @@ set -euo pipefail TARGET="${1:-all}" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" +SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android" SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios" -ANDROID_FILES=( - "StreamShimmerFrameLayout.kt" - "StreamShimmerViewManager.kt" -) -IOS_FILES=( - "StreamShimmerViewComponentView.h" - "StreamShimmerViewComponentView.mm" - "StreamShimmerView.swift" -) +sync_dir_contents() { + local src_dir="$1" + local dst_dir="$2" -copy_file() { - local src_file="$1" - local dst_file="$2" - if [ -e "$dst_file" ]; then - local src_inode - local dst_inode - src_inode=$(stat -f %i "$src_file" 2>/dev/null || echo "") - dst_inode=$(stat -f %i "$dst_file" 2>/dev/null || echo "") - if [ -n "$src_inode" ] && [ "$src_inode" = "$dst_inode" ]; then - return - fi + mkdir -p "$dst_dir" + + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "$src_dir"/ "$dst_dir"/ + else + find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -R "$src_dir"/. "$dst_dir"/ fi - cp "$src_file" "$dst_file" +} + +has_content() { + local dir="$1" + [ -d "$dir" ] && [ -n "$(find "$dir" -mindepth 1 -print -quit)" ] } sync_from_package() { local package_name="$1" - local android_src_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" - local ios_src_dir="$ROOT_DIR/$package_name/ios" - local missing=0 + local android_src_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared" + local ios_src_dir="$ROOT_DIR/$package_name/ios/shared" mkdir -p "$SHARED_ANDROID_DIR" "$SHARED_IOS_DIR" - for filename in "${ANDROID_FILES[@]}"; do - local src_file="$android_src_dir/$filename" - local dst_file="$SHARED_ANDROID_DIR/$filename" - if [ ! -f "$src_file" ]; then - echo "Missing Android shimmer source in $package_name (skipping reverse sync): $src_file" - missing=1 - continue - fi - copy_file "$src_file" "$dst_file" - done - - for filename in "${IOS_FILES[@]}"; do - local src_file="$ios_src_dir/$filename" - local dst_file="$SHARED_IOS_DIR/$filename" - if [ ! -f "$src_file" ]; then - echo "Missing iOS shimmer source in $package_name (skipping reverse sync): $src_file" - missing=1 - continue - fi - copy_file "$src_file" "$dst_file" - done + if has_content "$android_src_dir"; then + sync_dir_contents "$android_src_dir" "$SHARED_ANDROID_DIR" + else + echo "Skipping Android reverse sync for $package_name: no files in $android_src_dir" + fi - if [ "$missing" -eq 1 ]; then - return 0 + if has_content "$ios_src_dir"; then + sync_dir_contents "$ios_src_dir" "$SHARED_IOS_DIR" + else + echo "Skipping iOS reverse sync for $package_name: no files in $ios_src_dir" fi } @@ -77,12 +56,12 @@ case "$TARGET" in ;; all) # Prefer native-package as source if both are present. - if [ -f "$ROOT_DIR/native-package/ios/StreamShimmerView.swift" ]; then + if has_content "$ROOT_DIR/native-package/android/src/main/java/com/streamchatreactnative/shared" || has_content "$ROOT_DIR/native-package/ios/shared"; then sync_from_package "native-package" - elif [ -f "$ROOT_DIR/expo-package/ios/StreamShimmerView.swift" ]; then + elif has_content "$ROOT_DIR/expo-package/android/src/main/java/com/streamchatreactnative/shared" || has_content "$ROOT_DIR/expo-package/ios/shared"; then sync_from_package "expo-package" else - echo "No package shimmer sources found to sync from" + echo "No package shared native sources found to sync from" exit 1 fi ;; @@ -93,4 +72,4 @@ case "$TARGET" in ;; esac -echo "Synchronized shimmer native files from package mirror to shared-native for target: $TARGET" +echo "Synchronized shared native directories from package mirror to shared-native for target: $TARGET" diff --git a/package/scripts/sync-shimmer-native.sh b/package/scripts/sync-shimmer-native.sh index f915780716..8d09ded9d5 100644 --- a/package/scripts/sync-shimmer-native.sh +++ b/package/scripts/sync-shimmer-native.sh @@ -5,38 +5,39 @@ set -euo pipefail TARGET="${1:-all}" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -ANDROID_SRC_DIR="$ROOT_DIR/shared-native/android/src/main/java/com/streamchatreactnative" -IOS_SRC_DIR="$ROOT_DIR/shared-native/ios" - -copy_file() { - local src_file="$1" - local dst_file="$2" - if [ -e "$dst_file" ]; then - local src_inode - local dst_inode - src_inode=$(stat -f %i "$src_file" 2>/dev/null || echo "") - dst_inode=$(stat -f %i "$dst_file" 2>/dev/null || echo "") - if [ -n "$src_inode" ] && [ "$src_inode" = "$dst_inode" ]; then - return - fi +SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android" +SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios" + +sync_dir_contents() { + local src_dir="$1" + local dst_dir="$2" + + mkdir -p "$dst_dir" + + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "$src_dir"/ "$dst_dir"/ + else + find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -R "$src_dir"/. "$dst_dir"/ fi - rm -f "$dst_file" - cp "$src_file" "$dst_file" } copy_to_package() { local package_name="$1" - local android_dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative" - local ios_dst_dir="$ROOT_DIR/$package_name/ios" + local android_dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared" + local ios_dst_dir="$ROOT_DIR/$package_name/ios/shared" - mkdir -p "$android_dst_dir" - copy_file "$ANDROID_SRC_DIR/StreamShimmerFrameLayout.kt" "$android_dst_dir/StreamShimmerFrameLayout.kt" - copy_file "$ANDROID_SRC_DIR/StreamShimmerViewManager.kt" "$android_dst_dir/StreamShimmerViewManager.kt" + if [ -d "$SHARED_ANDROID_DIR" ]; then + sync_dir_contents "$SHARED_ANDROID_DIR" "$android_dst_dir" + else + echo "Skipping Android sync: missing $SHARED_ANDROID_DIR" + fi - mkdir -p "$ios_dst_dir" - copy_file "$IOS_SRC_DIR/StreamShimmerViewComponentView.h" "$ios_dst_dir/StreamShimmerViewComponentView.h" - copy_file "$IOS_SRC_DIR/StreamShimmerViewComponentView.mm" "$ios_dst_dir/StreamShimmerViewComponentView.mm" - copy_file "$IOS_SRC_DIR/StreamShimmerView.swift" "$ios_dst_dir/StreamShimmerView.swift" + if [ -d "$SHARED_IOS_DIR" ]; then + sync_dir_contents "$SHARED_IOS_DIR" "$ios_dst_dir" + else + echo "Skipping iOS sync: missing $SHARED_IOS_DIR" + fi } case "$TARGET" in @@ -57,4 +58,4 @@ case "$TARGET" in ;; esac -echo "Synchronized shimmer native files for target: $TARGET" +echo "Synchronized shared native directories for target: $TARGET" diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt b/package/shared-native/android/StreamShimmerFrameLayout.kt similarity index 100% rename from package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerFrameLayout.kt rename to package/shared-native/android/StreamShimmerFrameLayout.kt diff --git a/package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt b/package/shared-native/android/StreamShimmerViewManager.kt similarity index 100% rename from package/shared-native/android/src/main/java/com/streamchatreactnative/StreamShimmerViewManager.kt rename to package/shared-native/android/StreamShimmerViewManager.kt From 8132f05401ca1b2db57ee0d4e671c22208bc925a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 18:18:03 +0100 Subject: [PATCH 32/40] feat: add rudimentary conflict management --- .gitignore | 1 + examples/ExpoMessaging/package.json | 2 +- examples/SampleApp/package.json | 2 +- package/expo-package/package.json | 6 +- package/native-package/package.json | 6 +- package/package.json | 4 +- ...opies.sh => clean-shared-native-copies.sh} | 2 +- package/scripts/reconcile-shared-native.sh | 175 ++++++++++++++++++ ...himmer-native.sh => sync-shared-native.sh} | 0 package/scripts/sync-shimmer-from-package.sh | 75 -------- 10 files changed, 187 insertions(+), 86 deletions(-) rename package/scripts/{clean-shimmer-native-copies.sh => clean-shared-native-copies.sh} (91%) create mode 100755 package/scripts/reconcile-shared-native.sh rename package/scripts/{sync-shimmer-native.sh => sync-shared-native.sh} (100%) delete mode 100755 package/scripts/sync-shimmer-from-package.sh diff --git a/.gitignore b/.gitignore index 5ecbf803c8..3895765f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ index.android.bundle *.app *.DSYM.zip **/metrics/ +package/shared-native/.sync-state/ diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 2dc1ac6f74..31a1b37897 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "expo-router/entry", "scripts": { - "sync-native": "bash ../../package/scripts/sync-shimmer-from-package.sh expo-package && bash ../../package/scripts/sync-shimmer-native.sh expo-package", + "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh expo-package && bash ../../package/scripts/sync-shared-native.sh expo-package", "prestart": "yarn sync-native", "preandroid": "yarn sync-native", "preios": "yarn sync-native", diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 66893ae7ba..0a3b243ecf 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/GetStream/stream-chat-react-native.git" }, "scripts": { - "sync-native": "bash ../../package/scripts/sync-shimmer-from-package.sh native-package && bash ../../package/scripts/sync-shimmer-native.sh native-package", + "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh native-package && bash ../../package/scripts/sync-shared-native.sh native-package", "preandroid": "yarn sync-native", "android": "react-native run-android", "preios": "yarn sync-native", diff --git a/package/expo-package/package.json b/package/expo-package/package.json index af3012118a..4108730345 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -82,9 +82,9 @@ "expo-audio": "~0.4.6" }, "scripts": { - "postinstall": "if [ -f ../scripts/sync-shimmer-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shimmer-native.sh expo-package; fi", - "prepack": "bash ../scripts/sync-shimmer-native.sh expo-package && cp ../../README.md .", - "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh expo-package" + "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh expo-package; fi", + "prepack": "bash ../scripts/sync-shared-native.sh expo-package && cp ../../README.md .", + "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh expo-package" }, "codegenConfig": { "name": "StreamChatExpoSpec", diff --git a/package/native-package/package.json b/package/native-package/package.json index 9a779c5b91..1dcd41ad08 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -81,9 +81,9 @@ } }, "scripts": { - "postinstall": "if [ -f ../scripts/sync-shimmer-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shimmer-native.sh native-package; fi", - "prepack": "bash ../scripts/sync-shimmer-native.sh native-package && cp ../../README.md .", - "postpack": "rm README.md && bash ../scripts/clean-shimmer-native-copies.sh native-package" + "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi", + "prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .", + "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package" }, "devDependencies": { "react-native": "^0.79.3" diff --git a/package/package.json b/package/package.json index 6116733801..ffea4c198b 100644 --- a/package/package.json +++ b/package/package.json @@ -24,8 +24,8 @@ ], "scripts": { "install-all": "(yarn install --force && (cd native-package && yarn install --force) && (cd expo-package && yarn install --force))", - "shimmer:sync": "bash ./scripts/sync-shimmer-native.sh all", - "shimmer:clean-copies": "bash ./scripts/clean-shimmer-native-copies.sh all", + "shared-native:sync": "bash ./scripts/sync-shared-native.sh all", + "shared-native:clean-copies": "bash ./scripts/clean-shared-native-copies.sh all", "build": "rimraf lib && yarn run --silent build-translations && bob build && yarn run --silent copy-translations", "build-translations": "i18next-cli sync", "copy-translations": "echo '\u001b[34mℹ\u001b[0m Copying translation files to \u001b[34mlib/typescript/i18n\u001b[0m' && cp -R -f ./src/i18n ./lib/typescript/i18n && echo '\u001b[32m✓\u001b[0m Done Copying Translations'", diff --git a/package/scripts/clean-shimmer-native-copies.sh b/package/scripts/clean-shared-native-copies.sh similarity index 91% rename from package/scripts/clean-shimmer-native-copies.sh rename to package/scripts/clean-shared-native-copies.sh index f8cea28908..f4eb986d4e 100644 --- a/package/scripts/clean-shimmer-native-copies.sh +++ b/package/scripts/clean-shared-native-copies.sh @@ -32,4 +32,4 @@ case "$TARGET" in ;; esac -echo "Cleaned generated shimmer native copies for target: $TARGET" +echo "Cleaned generated shared native copies for target: $TARGET" diff --git a/package/scripts/reconcile-shared-native.sh b/package/scripts/reconcile-shared-native.sh new file mode 100755 index 0000000000..d66fa5b2ed --- /dev/null +++ b/package/scripts/reconcile-shared-native.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET="${1:-all}" + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +STATE_DIR="$ROOT_DIR/shared-native/.sync-state" +SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android" +SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios" + +sync_dir_contents() { + local src_dir="$1" + local dst_dir="$2" + + mkdir -p "$dst_dir" + + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "$src_dir"/ "$dst_dir"/ + else + find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -R "$src_dir"/. "$dst_dir"/ + fi +} + +hash_manifest_for_dir() { + local dir="$1" + local manifest_path="$2" + + mkdir -p "$(dirname "$manifest_path")" + : > "$manifest_path" + + if [ ! -d "$dir" ]; then + return + fi + + while IFS= read -r file; do + local rel_path="${file#$dir/}" + local hash + hash="$(shasum "$file" | awk '{print $1}')" + printf "%s\t%s\n" "$rel_path" "$hash" >> "$manifest_path" + done < <(find "$dir" -type f | LC_ALL=C sort) +} + +hash_for_path() { + local manifest_path="$1" + local rel_path="$2" + awk -F '\t' -v path="$rel_path" '$1 == path { print $2; found=1; exit } END { if (!found) print "-" }' "$manifest_path" +} + +sync_platform_with_conflict_detection() { + local package_name="$1" + local platform_name="$2" + local package_dir="$3" + local shared_dir="$4" + + mkdir -p "$STATE_DIR" + mkdir -p "$package_dir" "$shared_dir" + + local baseline_manifest="$STATE_DIR/${package_name}_${platform_name}.manifest" + local tmp_baseline_manifest + local tmp_shared_manifest + local tmp_package_manifest + local tmp_union_paths + local tmp_conflicts + + tmp_baseline_manifest="$(mktemp)" + tmp_shared_manifest="$(mktemp)" + tmp_package_manifest="$(mktemp)" + tmp_union_paths="$(mktemp)" + tmp_conflicts="$(mktemp)" + + if [ -f "$baseline_manifest" ]; then + cp "$baseline_manifest" "$tmp_baseline_manifest" + fi + + hash_manifest_for_dir "$shared_dir" "$tmp_shared_manifest" + hash_manifest_for_dir "$package_dir" "$tmp_package_manifest" + + cat "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" \ + | awk -F '\t' 'NF > 0 { print $1 }' \ + | LC_ALL=C sort -u > "$tmp_union_paths" + + local shared_changed=0 + local package_changed=0 + local conflict_count=0 + + while IFS= read -r rel_path; do + [ -z "$rel_path" ] && continue + + local baseline_hash + local shared_hash + local package_hash + baseline_hash="$(hash_for_path "$tmp_baseline_manifest" "$rel_path")" + shared_hash="$(hash_for_path "$tmp_shared_manifest" "$rel_path")" + package_hash="$(hash_for_path "$tmp_package_manifest" "$rel_path")" + + if [ "$shared_hash" != "$baseline_hash" ]; then + shared_changed=1 + fi + if [ "$package_hash" != "$baseline_hash" ]; then + package_changed=1 + fi + + if [ "$shared_hash" != "$package_hash" ] && [ "$shared_hash" != "$baseline_hash" ] && [ "$package_hash" != "$baseline_hash" ]; then + conflict_count=$((conflict_count + 1)) + printf "%s (shared-native=%s, package=%s, baseline=%s)\n" \ + "$rel_path" "$shared_hash" "$package_hash" "$baseline_hash" >> "$tmp_conflicts" + fi + done < "$tmp_union_paths" + + if [ "$conflict_count" -gt 0 ]; then + echo "Conflict detected for $package_name [$platform_name]." + echo "Both shared-native and package mirror changed the same file(s) differently since last sync:" + cat "$tmp_conflicts" + rm -f "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" "$tmp_union_paths" "$tmp_conflicts" + return 1 + fi + + if [ "$package_changed" -eq 1 ] && [ "$shared_changed" -eq 0 ]; then + sync_dir_contents "$package_dir" "$shared_dir" + echo "Applied $platform_name sync direction: package -> shared-native ($package_name)" + elif [ "$shared_changed" -eq 1 ] && [ "$package_changed" -eq 0 ]; then + sync_dir_contents "$shared_dir" "$package_dir" + echo "Applied $platform_name sync direction: shared-native -> package ($package_name)" + fi + + hash_manifest_for_dir "$shared_dir" "$baseline_manifest" + rm -f "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" "$tmp_union_paths" "$tmp_conflicts" +} + +sync_from_package() { + local package_name="$1" + local android_package_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared" + local ios_package_dir="$ROOT_DIR/$package_name/ios/shared" + + sync_platform_with_conflict_detection \ + "$package_name" \ + "android" \ + "$android_package_dir" \ + "$SHARED_ANDROID_DIR" + + sync_platform_with_conflict_detection \ + "$package_name" \ + "ios" \ + "$ios_package_dir" \ + "$SHARED_IOS_DIR" +} + +case "$TARGET" in + native-package) + sync_from_package "native-package" + ;; + expo-package) + sync_from_package "expo-package" + ;; + all) + # Prefer native-package when both are present. + if [ -d "$ROOT_DIR/native-package/android/src/main/java/com/streamchatreactnative/shared" ] || [ -d "$ROOT_DIR/native-package/ios/shared" ]; then + sync_from_package "native-package" + elif [ -d "$ROOT_DIR/expo-package/android/src/main/java/com/streamchatreactnative/shared" ] || [ -d "$ROOT_DIR/expo-package/ios/shared" ]; then + sync_from_package "expo-package" + else + echo "No package shared native sources found to sync from" + exit 1 + fi + ;; + *) + echo "Unknown target: $TARGET" + echo "Expected one of: native-package, expo-package, all" + exit 1 + ;; +esac + +echo "Reconciled package/shared-native directories with conflict checks for target: $TARGET" diff --git a/package/scripts/sync-shimmer-native.sh b/package/scripts/sync-shared-native.sh similarity index 100% rename from package/scripts/sync-shimmer-native.sh rename to package/scripts/sync-shared-native.sh diff --git a/package/scripts/sync-shimmer-from-package.sh b/package/scripts/sync-shimmer-from-package.sh deleted file mode 100755 index fa7a28365c..0000000000 --- a/package/scripts/sync-shimmer-from-package.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -TARGET="${1:-all}" - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android" -SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios" - -sync_dir_contents() { - local src_dir="$1" - local dst_dir="$2" - - mkdir -p "$dst_dir" - - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete "$src_dir"/ "$dst_dir"/ - else - find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + - cp -R "$src_dir"/. "$dst_dir"/ - fi -} - -has_content() { - local dir="$1" - [ -d "$dir" ] && [ -n "$(find "$dir" -mindepth 1 -print -quit)" ] -} - -sync_from_package() { - local package_name="$1" - local android_src_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared" - local ios_src_dir="$ROOT_DIR/$package_name/ios/shared" - - mkdir -p "$SHARED_ANDROID_DIR" "$SHARED_IOS_DIR" - - if has_content "$android_src_dir"; then - sync_dir_contents "$android_src_dir" "$SHARED_ANDROID_DIR" - else - echo "Skipping Android reverse sync for $package_name: no files in $android_src_dir" - fi - - if has_content "$ios_src_dir"; then - sync_dir_contents "$ios_src_dir" "$SHARED_IOS_DIR" - else - echo "Skipping iOS reverse sync for $package_name: no files in $ios_src_dir" - fi -} - -case "$TARGET" in - native-package) - sync_from_package "native-package" - ;; - expo-package) - sync_from_package "expo-package" - ;; - all) - # Prefer native-package as source if both are present. - if has_content "$ROOT_DIR/native-package/android/src/main/java/com/streamchatreactnative/shared" || has_content "$ROOT_DIR/native-package/ios/shared"; then - sync_from_package "native-package" - elif has_content "$ROOT_DIR/expo-package/android/src/main/java/com/streamchatreactnative/shared" || has_content "$ROOT_DIR/expo-package/ios/shared"; then - sync_from_package "expo-package" - else - echo "No package shared native sources found to sync from" - exit 1 - fi - ;; - *) - echo "Unknown target: $TARGET" - echo "Expected one of: native-package, expo-package, all" - exit 1 - ;; -esac - -echo "Synchronized shared native directories from package mirror to shared-native for target: $TARGET" From 6128b5633147a28a7d58deed9031ce7e5eed1a31 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 18:20:42 +0100 Subject: [PATCH 33/40] fix: revert testing changes in gallery --- package/src/components/Attachment/Gallery.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 82ea8e3031..9ce0c22066 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -444,15 +444,15 @@ const GalleryImageThumbnail = ({ ) : ( <> - {/**/} - {true ? : null} + + {isLoadingImage ? : null} )} From 309a9d16411f0bc9c7cd7ed1552c24ef7d170b94 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 18:21:59 +0100 Subject: [PATCH 34/40] fix: sdk size metrics workflow --- examples/SampleApp/fastlane/Fastfile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index 0efb03543f..b34b769159 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -184,8 +184,17 @@ lane :frameworks_sizes do yarn_all sh('yarn build') sh('yarn minify-bundle') + js_bundle_size = file_size(path: 'package/lib/module/bundle.min.js') - { js_bundle_size: js_bundle_size } + + # Include shared-native footprint so native only changes affect metrics from the shared directory. + native_shared_bytes = Dir.glob('package/shared-native/**/*').sum do |path| + File.file?(path) ? File.size(path) : 0 + end + + native_shared_size = native_shared_bytes / 1024.0 + + { js_bundle_size: js_bundle_size + native_shared_size } end end From c9b85eb4f7eac3c42f8c1f22f413dea2b53f9e2a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 9 Mar 2026 18:34:42 +0100 Subject: [PATCH 35/40] fix: revert sdk size metrics fix as it is blatantly wrong --- examples/SampleApp/fastlane/Fastfile | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index b34b769159..365d3534ee 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -186,15 +186,7 @@ lane :frameworks_sizes do sh('yarn minify-bundle') js_bundle_size = file_size(path: 'package/lib/module/bundle.min.js') - - # Include shared-native footprint so native only changes affect metrics from the shared directory. - native_shared_bytes = Dir.glob('package/shared-native/**/*').sum do |path| - File.file?(path) ? File.size(path) : 0 - end - - native_shared_size = native_shared_bytes / 1024.0 - - { js_bundle_size: js_bundle_size + native_shared_size } + { js_bundle_size: js_bundle_size } end end From 1bea62053614c51b04fb05a715b255494106a3e7 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 10 Mar 2026 10:53:23 +0100 Subject: [PATCH 36/40] fix: revert leeway for loading changes --- package/src/components/Attachment/Gallery.tsx | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 9ce0c22066..e5c1b75e30 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -1,12 +1,5 @@ -import React, { useEffect, useMemo, useRef } from 'react'; -import { - ImageErrorEventData, - NativeSyntheticEvent, - Pressable, - StyleSheet, - Text, - View, -} from 'react-native'; +import React, { useMemo } from 'react'; +import { ImageErrorEvent, Pressable, StyleSheet, Text, View } from 'react-native'; import type { Attachment, LocalMessage } from 'stream-chat'; @@ -74,8 +67,6 @@ export type GalleryPropsWithContext = Pick { const { additionalPressableProps, @@ -395,7 +386,6 @@ const GalleryImageThumbnail = ({ setLoadingImage, setLoadingImageError, } = useLoadingImage(); - const loadingIndicatorTimerRef = useRef | null>(null); const { theme: { @@ -405,38 +395,23 @@ const GalleryImageThumbnail = ({ const styles = useStyles(); - const clearLoadingIndicatorTimer = useStableCallback(() => { - if (loadingIndicatorTimerRef.current) { - clearTimeout(loadingIndicatorTimerRef.current); - loadingIndicatorTimerRef.current = null; - } - }); - const onLoadStart = useStableCallback(() => { - clearLoadingIndicatorTimer(); - setLoadingImageError(false); - loadingIndicatorTimerRef.current = setTimeout(() => { - setLoadingImage(true); - loadingIndicatorTimerRef.current = null; - }, IMAGE_LOADING_INDICATOR_DELAY_MS); - }); - - const onLoadEnd = useStableCallback(() => { - clearLoadingIndicatorTimer(); - setLoadingImage(false); setLoadingImageError(false); + setLoadingImage(true); }); - const onError = useStableCallback( - ({ nativeEvent: { error } }: NativeSyntheticEvent) => { - clearLoadingIndicatorTimer(); - console.warn(error); + const onLoad = useStableCallback(() => { + setTimeout(() => { setLoadingImage(false); - setLoadingImageError(true); - }, - ); + setLoadingImageError(false); + }, 0); + }); - useEffect(() => clearLoadingIndicatorTimer, [clearLoadingIndicatorTimer]); + const onError = useStableCallback(({ nativeEvent: { error } }: ImageErrorEvent) => { + console.warn(error); + setLoadingImage(false); + setLoadingImageError(true); + }); return ( @@ -446,7 +421,7 @@ const GalleryImageThumbnail = ({ <> Date: Tue, 10 Mar 2026 11:27:40 +0100 Subject: [PATCH 37/40] fix: tests --- .../components/Attachment/__tests__/Gallery.test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/package/src/components/Attachment/__tests__/Gallery.test.js b/package/src/components/Attachment/__tests__/Gallery.test.js index e40ee4cb98..23a2ccdade 100644 --- a/package/src/components/Attachment/__tests__/Gallery.test.js +++ b/package/src/components/Attachment/__tests__/Gallery.test.js @@ -20,6 +20,15 @@ import { Chat } from '../../Chat/Chat'; import { MessageList } from '../../MessageList/MessageList'; describe('Gallery', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + const user1 = generateUser(); const getComponent = async (attachments = []) => { @@ -287,10 +296,10 @@ describe('Gallery', () => { expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy(); }); - fireEvent(screen.getByLabelText('Gallery Image'), 'loadEnd'); + fireEvent(screen.getByLabelText('Gallery Image'), 'onLoad'); await waitFor(() => { expect(screen.queryByLabelText('Image Loading Indicator')).toBeNull(); }); expect(screen.getByLabelText('Gallery Image')).toBeTruthy(); - }); + }, 20000); }); From 4c2c7d7b8863754c5ae1810c98dfdd2ec6382711 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 10 Mar 2026 12:16:07 +0100 Subject: [PATCH 38/40] chore: update Podfile.lock --- examples/SampleApp/ios/Podfile.lock | 160 ++++++++++++++-------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index 18b1bdbe28..acad889774 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -3640,9 +3640,9 @@ SPEC CHECKSUMS: hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - NitroModules: 8849240f6ee6d3b295514112437e3b09e855cb67 - NitroSound: fe46960c89410e62e05e9a709d8bf28a8202d1b3 - op-sqlite: b9a4028bef60145d7b98fbbc4341c83420cdcfd5 + NitroModules: 01ae20fc1e8fc9a3b088ab8ed06ab92527a04f0d + NitroSound: 347b6a21f2e7d5601c92ef81cec7836f8f8be44c + op-sqlite: a7e46cfdaebeef219fd0e939332967af9fe6d406 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f @@ -3651,94 +3651,94 @@ SPEC CHECKSUMS: RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f React: e7a4655b09d0e17e54be188cc34c2f3e2087318a React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b - React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7 - React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2 - React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac + React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542 + React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553 + React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691 React-debug: ab52e07f7148480ea61c5e9b68408d749c6e2b8f - React-defaultsnativemodule: fab7bf1b5ce1f3ed252aa4e949ec48a8df67d624 - React-domnativemodule: 735fa5238cceebceeecc18f9f4321016461178cf - React-Fabric: c75719fc8818049c3cf9071f0619af988b020c9f - React-FabricComponents: 74a381cc0dfaf2ec3ee29f39ef8533a7fd864b83 - React-FabricImage: 9a3ff143b1ac42e077c0b33ec790f3674ace5783 - React-featureflags: e1eca0727563a61c919131d57bbd0019c3bdb0f0 - React-featureflagsnativemodule: 692211fd48283b2ddee3817767021010e2f1788e - React-graphics: 759b02bde621f12426c1dc1ae2498aaa6b441cd7 - React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b - React-idlecallbacksnativemodule: 731552efc0815fc9d65a6931da55e722b1b3a397 - React-ImageManager: 2c510a480f2c358f56a82df823c66d5700949c96 - React-jserrorhandler: 411e18cbdcbdf546f8f8707091faeb00703527c1 - React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9 - React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb - React-jsinspector: 2f0751e6a4fb840f4ed325384d0795a9e9afaf39 - React-jsinspectorcdp: 71c48944d97f5f20e8e144e419ddf04ffa931e93 - React-jsinspectornetwork: 102f347669b278644cc9bb4ebf2f90422bd5ccef - React-jsinspectortracing: 0f6f2ec7f3faa9dc73d591b24b460141612515eb - React-jsitooling: b557f8e12efdaf16997e43b0d07dbd8a3fce3a5b - React-jsitracing: f9a77561d99c0cd053a8230bab4829b100903949 - React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb - React-Mapbuffer: 230c34b1cabd1c4815726c711b9df221c3d3fbfb - React-microtasksnativemodule: 29d62f132e4aba34ebb7f2b936dde754eb08971b - react-native-blob-util: cbd6b292d0f558f09dce85e6afe68074cd031f3e - react-native-blur: 92cc889e5515dcb0746d5a24207a28eea2f6bb65 - react-native-cameraroll: 00057cc0ec595fdbdf282ecfb931d484b240565f - react-native-document-picker: c5fa18e9fc47b34cfbab3b0a4447d0df918a5621 - react-native-geolocation: eb39c815c9b58ddc3efb552cafdd4b035e4cf682 - react-native-image-picker: e479ec8884df9d99a62c1f53f2307055ad43ea85 - react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225 - react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-safe-area-context: 7fd4c2c8023da8e18eaa3424cb49d52f626debee - react-native-video: 71973843c2c9ac154c54f95a5a408fd8b041790e - React-NativeModulesApple: d061f458c3febdf0ac99b1b0faf23b7305974b25 + React-defaultsnativemodule: 291d2b0a93c399056121f4f0acc7f46d155a38ec + React-domnativemodule: c4968302e857bd422df8eec50a3cd4d078bd4ac0 + React-Fabric: 7e3ba48433b87a416052c4077d5965aff64cb8c9 + React-FabricComponents: 21de255cf52232644d12d3288cced1f0c519b25d + React-FabricImage: 15a0961a0ab34178f1c803aa0a7d28f21322ffc3 + React-featureflags: 4e5dad365d57e3c3656447dfdad790f75878d9f4 + React-featureflagsnativemodule: 5eac59389131c2b87d165dac4094b5e86067fabb + React-graphics: 2f9b3db89f156afd793da99f23782f400f58c1ee + React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761 + React-idlecallbacksnativemodule: 1d7e1f73b624926d16db956e87c4885ef485664a + React-ImageManager: 8b6066f6638fba7d4a94fbd0b39b477ea8aced58 + React-jserrorhandler: e5a4626d65b0eda9a11c43a9f14d0423d8a7080d + React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c + React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826 + React-jsinspector: 094e3cb99952a0024fa977fa04706e68747cba18 + React-jsinspectorcdp: dca545979146e3ecbdc999c0789dab55beecc140 + React-jsinspectornetwork: 0a105fe74b0b1a93f70409d955c8a0556dc17c59 + React-jsinspectortracing: 76088dd78a2de3ea637a860cdf39a6d9c2637d6b + React-jsitooling: a2e1e87382aae2294bc94a6e9682b9bc83da1d36 + React-jsitracing: 45827be59e673f4c54175c150891301138846906 + React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce + React-Mapbuffer: 8f620d1794c6b59a8c3862c3ae820a2e9e6c9bb0 + React-microtasksnativemodule: dcf5321c9a41659a6718df8a5f202af1577c6825 + react-native-blob-util: a511afccff6511544ebf56928e6afdf837b037a7 + react-native-blur: ecdc987ab8d8fba95abef14551f033376872d0a6 + react-native-cameraroll: 8c3ba9b6f511cf645778de19d5039b61d922fdfb + react-native-document-picker: b37cf6660ad9087b782faa78a1e67687fac15bfd + react-native-geolocation: b7f68b8c04e36ee669c630dbc48dd42cf93a0a41 + react-native-image-picker: 9bceb747cd6cde22a3416ffdc819d11b5b11f156 + react-native-maps: 9febd31278b35cd21e4fad2cf6fa708993be5dab + react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 + react-native-safe-area-context: 32293dc61d1b92ccf892499ab6f8acfd609f9aef + react-native-video: 4da16bfca01a02aa2095e40683d74f2d6563207c + React-NativeModulesApple: 342e280bb9fc2aa5f61f6c257b309a86b995e12d React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1 - React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f - React-performancetimeline: 53bdf62ff49a9b0c4bd4d66329fdcf28d77c1c9d + React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9 + React-performancetimeline: 3ac316a346fe3d48801a746b754dd8f5b5146838 React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e - React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89 - React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb - React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0 - React-RCTFabric: fad6230640c42fb8cda29b1d0759f7a1fb8cc677 - React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec - React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9 - React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce - React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482 - React-RCTRuntime: c69b86dc60dcc7297318097fc60bd8e40b050f74 - React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884 - React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74 - React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440 + React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d + React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251 + React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b + React-RCTFabric: 04d1cf11ee3747a699260492e319e92649d7ac88 + React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae + React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab + React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670 + React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc + React-RCTRuntime: 52b28e281aba881e1f94ee8b16611823b730d1c5 + React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307 + React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9 + React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72 React-rendererconsistency: aba18fa58a4d037004f6bed6bb201eb368016c56 - React-renderercss: c7c140782f5f21103b638abfde7b3f11d6a5fd7e - React-rendererdebug: 111519052db9610f1b93baf7350c800621df3d0c + React-renderercss: b490bd53486a6bae1e9809619735d1f2b2cabd7f + React-rendererdebug: 8db25b276b64d5a1dbf05677de0c4ff1039d5184 React-rncore: 22f344c7f9109b68c3872194b0b5081ca1aee655 - React-RuntimeApple: 30d20d804a216eb297ccc9ce1dc9e931738c03b1 - React-RuntimeCore: 6e1facd50e0b7aed1bc36b090015f33133958bb6 + React-RuntimeApple: 19bdabedda0eeb70acb71e68bfc42d61bbcbacd9 + React-RuntimeCore: 11bf03bdbd6e72857481c46d0f4eb9c19b14c754 React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5 - React-RuntimeHermes: 222268a5931a23f095565c4d60e2673c04e2178a - React-runtimescheduler: aea93219348ba3069fe6c7685a84fe17d3a4b4ee + React-RuntimeHermes: d8f736d0a2d38233c7ec7bd36040eb9b0a3ccd8c + React-runtimescheduler: 0c95966d030c8ebbebddaab49630cda2889ca073 React-timing: 42e8212c479d1e956d3024be0a07f205a2e34d9d - React-utils: 0ebf25dd4eb1b5497933f4d8923b862d0fe9566f - ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989 - ReactCodegen: f8ae44cfcb65af88f409f4b094b879591232f12c - ReactCommon: a237545f08f598b8b37dc3a9163c1c4b85817b0b - RNCAsyncStorage: afe7c3711dc256e492aa3a50dcac43eecebd0ae5 - RNCClipboard: f09308447254536ab20b243607766796efdf210c - RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 - RNFBApp: df5caad9f64b6bc87f8a0b110e6bc411fb00a12b - RNFBMessaging: 6586f18ab3411aeb3349088c19fe54283d39e529 - RNGestureHandler: 6f2448500f339bc93dc589a5fd4da00e6d0033b2 - RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 - RNReactNativeHapticFeedback: 8bd4a2ba7c3daeb5d2acfceb8b61743f203076d0 - RNReanimated: 0d996442e746ee9d947f13914fcc17b43b17c445 - RNScreens: 5ca475eb30f4362c5808f3ff4a1c7b786bcd878e - RNShare: 083f05646b9534305999cf20452bd87ca0e8b0b0 - RNSVG: fd433fe5da0f7fee8c78f5865f29ab37321dbd7f - RNWorklets: a382db09224b0f6fda1c72c4c9bf1abfac9b3db8 + React-utils: a185f723baa0c525c361e6c281a846d919623dbe + ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0 + ReactCodegen: 4928682e20747464165effacc170019a18da953c + ReactCommon: ec1cdf708729338070f8c4ad746768a782fd9eb1 + RNCAsyncStorage: f30b3a83064e28b0fc46f1fbd3834589ed64c7b9 + RNCClipboard: 8e5237c79dafacea5b7adf4c3ab39a4236b5ef7e + RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87 + RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e + RNFBMessaging: de62448d205095171915d622ed5fb45c2be5e075 + RNGestureHandler: 0c0d36c0f3c17fc755543fad1c182e1cd541f898 + RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 + RNReactNativeHapticFeedback: d39b9a5b334ce26f49ca6abe9eea8b3938532aee + RNReanimated: e3dd9527a9614e1c9e127018cca9486f2c13b2a9 + RNScreens: 6a2d1ff4d263d29d3d3db9f3c19aad2f99fdd162 + RNShare: 9d801eafd9ae835f51bcae6b5c8de9bf3389075b + RNSVG: bc7ccfe884848ac924d2279d9025d41b5f05cb0c + RNWorklets: 4bd2a43ae826633e5e0a92953fce2eb8265759d4 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: 19671d89c0503d527f1d5d7ff3f7a5359de7dbd5 - Teleport: 9ae328b3384204b23236e47bd0d8e994ce6bdb48 + stream-chat-react-native: 3a5d663e1d32afb54a3afba3691f08be65a20374 + Teleport: c089481dd2bb020e3dced39b7f8849b93d1499f6 Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 From 9e7a6bdb3a6f52a0e81c35afeefd1023070e9b1b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 10 Mar 2026 16:08:30 +0100 Subject: [PATCH 39/40] fix: lock files --- examples/ExpoMessaging/package.json | 1 + examples/ExpoMessaging/yarn.lock | 46 +++++------------------ examples/TypeScriptMessaging/package.json | 4 ++ 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 31a1b37897..3fc4d38da1 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -48,6 +48,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", + "react-native-teleport": "^1.0.2", "react-native-worklets": "0.5.1", "stream-chat-expo": "link:../../package/expo-package", "stream-chat-react-native-core": "link:../../package" diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index 5513edc778..c74a42d065 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -4264,22 +4264,6 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonwebtoken@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" - integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - jsonwebtoken@^9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" @@ -4296,15 +4280,6 @@ jsonwebtoken@^9.0.3: ms "^2.1.1" semver "^7.5.4" -jwa@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" - integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== - dependencies: - buffer-equal-constant-time "^1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - jwa@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" @@ -4314,14 +4289,6 @@ jwa@^2.0.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - jws@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" @@ -5568,6 +5535,11 @@ react-native-svg@15.12.1: css-tree "^1.1.3" warn-once "0.1.1" +react-native-teleport@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-native-teleport/-/react-native-teleport-1.0.2.tgz#f5bed0534acba29787a6e3707513eed91cb4f8ea" + integrity sha512-+DE9N9JMxulUZwREDPBYl10Urmqocvzw++/BXzC34YzaHaDfbmgvr/KFJjGYoZhJUMcOJjBC9OxESH6+yzvxJA== + react-native-url-polyfill@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589" @@ -6081,9 +6053,9 @@ stream-chat@^9.35.1: ws "^8.18.1" stream-chat@^9.9.0: - version "9.20.3" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.20.3.tgz#5f47d6f46d146202c743282f5fb7350f4a640922" - integrity sha512-206Lea0ZAVWbfYZkIwLG5m+++ELD3f8EAEL/YzbMDL++E2vU2WhQ2d1HNb1ROXURZUF0Sy845htTw1rwnahomw== + version "9.36.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.36.0.tgz#154e0d6bdf8b15e97a6d9718c655d2ede34f6f25" + integrity sha512-D1b5THI4UbnvsEcJyUv1tUIgK6lCYT+aStrV+87mdrM9owX+WUpKaWFkxz/Ug+DOrJtTazvfuzvpJMyDi82NXA== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" @@ -6091,7 +6063,7 @@ stream-chat@^9.9.0: base64-js "^1.5.1" form-data "^4.0.4" isomorphic-ws "^5.0.0" - jsonwebtoken "^9.0.2" + jsonwebtoken "^9.0.3" linkifyjs "^4.3.2" ws "^8.18.1" diff --git a/examples/TypeScriptMessaging/package.json b/examples/TypeScriptMessaging/package.json index 3f9e55baae..c2efffc829 100644 --- a/examples/TypeScriptMessaging/package.json +++ b/examples/TypeScriptMessaging/package.json @@ -3,8 +3,12 @@ "version": "0.0.1", "private": true, "scripts": { + "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh native-package && bash ../../package/scripts/sync-shared-native.sh native-package", + "preandroid": "yarn sync-native", "android": "react-native run-android", + "preios": "yarn sync-native", "ios": "react-native run-ios", + "prestart": "yarn sync-native", "start": "react-native start", "test": "jest", "lint": "eslint .", From 98e8edfcba0b819c4550a558e1e4c062b119c996 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 10 Mar 2026 16:08:43 +0100 Subject: [PATCH 40/40] fix: default channel list item component --- package/src/components/ChannelList/ChannelList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/components/ChannelList/ChannelList.tsx b/package/src/components/ChannelList/ChannelList.tsx index e9570aafd0..f2a91b0cfb 100644 --- a/package/src/components/ChannelList/ChannelList.tsx +++ b/package/src/components/ChannelList/ChannelList.tsx @@ -28,7 +28,7 @@ import { import { useChatContext } from '../../contexts/chatContext/ChatContext'; import { SwipeRegistryProvider } from '../../contexts/swipeableContext/SwipeRegistryContext'; import type { ChannelListEventListenerOptions } from '../../types/types'; -import { ChannelPreview } from '../ChannelPreview/ChannelPreview'; +import { ChannelPreviewMessenger } from '../ChannelPreview/ChannelPreviewMessenger'; import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator'; import { LoadingErrorIndicator as LoadingErrorIndicatorDefault } from '../Indicators/LoadingErrorIndicator'; @@ -279,7 +279,7 @@ export const ChannelList = (props: ChannelListProps) => { onRemovedFromChannel, onSelect, options = DEFAULT_OPTIONS, - Preview = ChannelPreview, + Preview = ChannelPreviewMessenger, getChannelActionItems, PreviewAvatar, PreviewMessage,