stream-chat-react-native icon indicating copy to clipboard operation
stream-chat-react-native copied to clipboard

[🐛] Image attachment is not rendering properly inside message.

Open Saad-Bashar opened this issue 11 months ago • 10 comments

We are customising the AttachButton as per doc and uploading new images using uploadNewImage. This works fine. However, when the image is attached inside the message list, there is a weird grey view appearing bottom of the image.

Please note that when user goes back and come back to the chat screen again, the image is rendered fine. Only when the image is attached instantly in a message then the issue occurs. I believe this happens mainly when I am taking the picture in Potrait mode.

This is after I attach a photo from camera, Image

But the image renders fine when I come back to chat, Image

This is how my code looks,

// Custom attach button component
const CustomAttachButton = () => {
  const {showActionSheetWithOptions} = useActionSheet();
  const {uploadNewImage} = useMessageInputContext();

  const compressAndUploadImage = async imageFile => {
    try {
      const compressedUri = await ImageCompressor.compress(imageFile.uri, {
        compressionMethod: 'auto',
      });

      return uploadNewImage({
        name: imageFile.fileName,
        type: imageFile.type,
        uri: compressedUri,
      });
    } catch (error) {
      // Fallback to original image if compression fails
      return uploadNewImage({
        name: imageFile.fileName,
        type: imageFile.type,
        uri: imageFile.uri,
      });
    }
  };

  const pickImageFromGallery = async () => {
    try {
      const images = await launchImageLibrary({
        selectionLimit: 1,
        mediaType: 'photo',
      });

      if (images?.assets?.length > 0) {
        const selectedImage = images.assets[0];
        await compressAndUploadImage(selectedImage);
      }
    } catch (error) {
      console.error('Error picking image from gallery:', error);
    }
  };

  const pickImageFromCamera = async () => {
    try {
      if (Platform.OS === 'android') {
        const granted = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.CAMERA,
          {
            title: 'Camera Permission',
            message: 'ShiftCare needs camera permission to take photos',
            buttonNeutral: 'Ask Me Later',
            buttonNegative: 'Cancel',
            buttonPositive: 'OK',
          },
        );

        if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
          console.log('Camera permission denied');
          return;
        }
      }

      const images = await launchCamera({
        mediaType: 'photo',
      });

      if (images?.assets?.length > 0) {
        const selectedImage = images.assets[0];
        await compressAndUploadImage(selectedImage);
      }
    } catch (error) {
      console.error('Error picking image from camera:', error);
    }
  };

  const onPress = () => {
    showActionSheetWithOptions(
      {
        cancelButtonIndex: 2,
        destructiveButtonIndex: 2,
        options: ['Photo Library', 'Camera', 'Cancel'],
      },
      buttonIndex => {
        switch (buttonIndex) {
          case 0:
            pickImageFromGallery();
            break;
          case 1:
            pickImageFromCamera();
            break;
          default:
            break;
        }
      },
    );
  };

  return <AttachButton handleOnPress={onPress} />;
};


// Chat component
return (
    <SafeAreaView edges={['bottom']} className="flex-1">
      <ActionSheetProvider>
        <ChannelComponent
          key={channel.cid}
          supportedReactions={supportedReactions}
          hasFilePicker={false}
          MessageFooter={featureReadReceipts ? CustomMessageFooter : () => null}
          CommandsButton={() => null}
          AttachButton={CustomAttachButton}
          channel={channel}>
          <MessageList />
          <MessageInput />
          <BottomSpacing />
        </ChannelComponent>
      </ActionSheetProvider>
    </SafeAreaView>
  );

Saad-Bashar avatar Feb 20 '25 04:02 Saad-Bashar

Hey @Saad-Bashar, I tried to triage the issue on my end by sending a similar photo on the Android emulator and capturing the image, but I couldn't reproduce it. I am attaching a video. Do you have any added customization?

https://github.com/user-attachments/assets/84110e82-d401-432a-944d-28e131c89430

khushal87 avatar Mar 05 '25 06:03 khushal87

Hey @Saad-Bashar, are you still facing this issue, or can we close it?

khushal87 avatar Mar 17 '25 15:03 khushal87

Yes, we are still having this issue, unfortunately. This one mostly started to happen when we started to use uploadNewFile instead of the built-in way. We are using react-native-image-picker. We don't have any other customizations. These are the versions,

"@stream-io/flat-list-mvcp": "^0.10.3",
"stream-chat": "^8.44.0",
"stream-chat-react-native": "^5.41.4"

Saad-Bashar avatar Mar 17 '25 23:03 Saad-Bashar

same problem here

marijang avatar Mar 27 '25 18:03 marijang

?

marijang avatar Apr 07 '25 12:04 marijang

Same problem here when I uploaded the picture, this issue occurred. it didn't do it with that horizontal picture, only occurred in the case I uploaded a vertical picture. I am using iOS Emulator

Image

mach-an avatar May 08 '25 18:05 mach-an

I'm having the same issue and have done no UI customization.

@bridgetrosefitz figured out that if you adjust this styling property, it looks better:

theme.messageSimple.gallery = {
    ...theme.messageSimple.gallery,
    galleryItemColumn: {
      justifyContent: "center",
    }
}

But using the sendMessage API still doesn't recognize vertical images, and makes every attached image horizontal:

https://getstream.io/chat/docs/react-native/send_message/#complex-example

"stream-chat-expo": "^6.7.4",

Image

After applying the style fix:

Image

kevinmcalear avatar May 11 '25 01:05 kevinmcalear

Hello, @kevinmcalear, thanks for considering my problem

I did like this:

import {defaultTheme} from 'stream-chat-react-native';

const customTheme = {
  ...defaultTheme,
  messageSimple: {
    ...defaultTheme.messageSimple,
    gallery: {
      ...defaultTheme.messageSimple.gallery,
      galleryItemColumn: {
        justifyContent: 'center',
      },
    },
  },
};

  return channel ? (
    <SafeAreaView edges={['top', 'bottom']} style={styles.container}>
      <Header rightButtons={headerIcons} />
      {!loading ? (
        <View style={{flex: 1}}>
          <TouchableOpacity
            onPress={() =>
              navigateToProfile(chatPartnerId || chatPartnerUserId)
            }>
            <View style={styles.partnerHeader}>
              <PartnerHeader
                chatPartnerId={chatPartnerId || chatPartnerUserId}
              />
              <TDText variant="h3" paddingLeft={5}>
                {chatPartnerName} {!clientIsReady ? ' (Pending)' : ''}
              </TDText>
              <OnlineStatus
                status={chatPartnerOnline ? 'online' : 'offline'}
                hasLabel={false}
              />
            </View>
          </TouchableOpacity>
          <KeyboardAvoidingView
            behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
            style={{flex: 1}}
            keyboardVerticalOffset={Platform.OS === 'ios' ? 135 : 120}>
            <Channel
              theme={customTheme} // Pass the custom theme here
              EmptyStateIndicator={CustomEmptyStateIndicator}
              MessageAvatar={() => (
                <TruDateMessageAvatar
                  onPress={() =>
                    navigateToProfile(chatPartnerId || chatPartnerUserId)
                  }
                  userId={chatPartnerId || chatPartnerUserId}
                />
              )}
              channel={channel}
              maxNumberOfFiles={1}
              FileAttachment={ChatFileAttachment}
              allowThreadMessagesInChannel={false}
              reactionListPosition="top"
              ReactionList={CustomReactionList}
              supportedReactions={supportedReactions}
              disableTypingIndicator
              giphyEnabled={false}
              hasCommands={false}
              hasImagePicker={Platform.OS === 'ios' ? true : true}
              AttachButton={CustomAttachButton}
              hasFilePicker={false}
              initialScrollToFirstUnreadMessage
              keyboardVerticalOffset={Platform.OS === 'ios' ? 120 : 100}
              messageId={messageId}
              mentionAllAppUsersEnabled={false}
              autoCompleteTriggerSettings={() => ({})}
              messageActions={({
                isMyMessage,
                copyMessage,
                flagMessage,
                messageReactions,
                retry,
              }) =>
                isMyMessage
                  ? [copyMessage, retry]
                  : [flagMessage, messageReactions]
              }
              NetworkDownIndicator={() => null}>
              <MessageList<StreamChatGenerics> />
              <MessageInput />
            </Channel>
          </KeyboardAvoidingView>
        </View>
      ) : (
        ''
      )}
      <ReportUserActionSheet
        show={showReport}
        userId={(chatPartnerId || chatPartnerUserId)?.replace('-', '|')}
        reportUserName={chatPartnerName || 'user'}
        setIsLoading={setLoading}
        onHide={() => setShowReport(false)}
        onPressSafetyCenter={() => navigation.navigate('SafetyCenter')}
      />
    </SafeAreaView>
  ) : invitationSent ? (
    <View>
      <Text>Invitation to chat was sent and is awaiting acceptance</Text>
    </View>
  ) : null;
};

but this code didn't fix the problem. Please kindly guide me. thanks

mach-an avatar May 12 '25 17:05 mach-an

I ran into this problem today.

It's occurring because of the image dimensions are not being set to the image instance after the image is taken with the camera. There is code to manage this in the takeAndUploadImage method which is used by the default AttachButton. Specifically the setting of dimensions is in code for the takePhoto function here.

In order to solve this when using a custom AttachButton button I extracted the logic and used it to set the width and height to be passed into uploadNewImage.

type Dimenions = { width?: number; height?: number };
/**
 * Gets image size no matter if it came from camera or gallery
 * Copied from https://github.com/GetStream/stream-chat-react-native/blob/develop/package/native-package/src/optionalDependencies/takePhoto.ts
 */
async function getImageDimensions(image: ImagePickerAsset) {
  let size: Dimenions = {};
  if (Platform.OS === 'android') {
    const getSize = (): Promise<Dimenions> =>
      new Promise((resolve) => {
        Image.getSize(image.uri, (width, height) => {
          resolve({ height, width });
        });
      });

    try {
      const { height, width } = await getSize();
      size.height = height;
      size.width = width;
    } catch (e) {
      console.warn('Error get image size of picture caputred from camera ', e);
    }
  } else {
    size = {
      height: image.height,
      width: image.width,
    };
  }

  return size;
}

Usage:

const dimensions = await getImageDimensions(image);
const result = await uploadNewImage({
  uri: image.uri,
  name: image.fileName ?? image.uri.split('/').pop() ?? 'image.jpg',
  size: image.fileSize!,
  type: image.mimeType!,
  ...dimensions,
});

lukeggchapman avatar Jun 03 '25 05:06 lukeggchapman

Thanks very much, @lukeggchapman Your code fixed my problem. 👍

mach-an avatar Jun 03 '25 22:06 mach-an