Solvedreact native gifted chat Multiline does not grow TextInput until several characters into new line

Issue Description

First of all, I'm a big fan of the chat! Thank you to all who have contributed. πŸŽ‰

We're seeing an issue where the TextInput does not grow vertically until several characters into the new line (see attached gif). So, it'll grow to two lines on, say, the 5th character after the wrap. Same with the third line, fourth line, etc. After a few characters, the TextInput does grow to show the line.

Steps to Reproduce / Code Snippets

Repro Steps:

  1. Type until the end of the first line.
  2. Type 1-3 characters into the second line, so that the text wraps. See the TextInput has not yet grown.
  3. Type a few more characters. See that TextInput grows to show the second line.
      <GiftedChat
        alignTop
        renderSend={() => null}
        renderAccessory={({ text, onSend }) => (
          <MessengerInputButtons
            sendButton={sendButton}
            onSend={onSend}
            text={text}
            showMediaPrompt={this.props.showMediaPrompt}
          />
        )}
        renderBubble={(props) => <ChatBubble {...props} />}
        messages={formattedMessages}
        renderMessageText={(props) => <MessageText {...props} />}
        renderMessageImage={(props) => <MessageImage {...props} />}
        renderTime={() => null}
        renderDay={(props) => <ChatTimeStamp {...props} />}
        renderAvatar={(msgs) => (
          <FailedMessage
            showRetryActionSheet={this.props.showRetryActionSheet}
            messages={msgs}
          />
        )}
        showUserAvatar
        placeholder={'Write a message...'}
        placeholderTextColor={colors.brightGrey}
        textInputStyle={styles.textInputStyle}
        renderInputToolbar={(props) => (
          <InputToolbar
            {...props}
            containerStyle={styles.inputContainerStyle}
          />
        )}
        onSend={sendButton ? () => {} : message => this._onSend(message)}
        minInputToolbarHeight={this.determineMinInputHeight()}
        keyboardShouldPersistTaps={'never'}
        imageStyle={{ margin: 0 }}
        user={{ _id: this.props.user.id }}
        textInputProps={{
          selectionColor: colors.seafoam,
          marginTop: 12,
          marginLeft: 0,
          marginBottom: interfaceHelper.styleSwitch({
            xphone: 21, iphone: 6, android: 6,
          }),
          marginRight: 0,
          paddingTop: 0,
        }}
      />

Expected Results

When text wrapped, we expect the TextInput to grow on the first new character of the new line, not on the, say, 5th or 6th character of the new line.

Additional Information

Apr-08-2020 00-41-50

  • Nodejs version: v8.11.4
  • React version: 16.4.1
  • React Native version: 0.56.0
  • react-native-gifted-chat version: 0.13.0
  • Platform(s) (iOS, Android, or both?): iOS
19 Answers

βœ”οΈAccepted Answer

This was not an easy one. It took me a lot of work to find and fix the issue too + animate the input nicely. You simple can't use padding for your input, it will break the multi-line calculation (thats why you have this glitch)

Since I really wanted a round input + be flexible with animation, I ended up providing my own InputToolbar and I copied the <Composer> from gifted chat and reworked it, rendering my own .

The roudness and padding comes from my <View> (and not from the Input) with border and all that padding around it, too. The input is a dead simple input without any borders or padding. But now, setting lineHeight and fontSize won't break the calculation, since the input has no padding :)

I won't give any support on this, but here is my custom Composer, which you can add as custom prop to <GiftedChat>. Play with it until it fits your needs.

It will prob. not work right away for you and you need to know that I use the UIManager for the animation, which have to be activated for android. (its already in the code)
In my case, it works butter smooth on iOS and very good on low end Android devices (also butter smooth on high end devices).

If you don't want that smooth nice animation, delete that parts and just have a look how I fixed the issue.

Here is a video of my input:
https://streamable.com/qedbpb

TL;DR: dont add padding to your input, add the padding you need to a wrapping view.

import PropTypes from "prop-types";
import React from "react";
import { View, Platform, StyleSheet, TextInput, LayoutAnimation, UIManager } from "react-native";
import { MIN_COMPOSER_HEIGHT, DEFAULT_PLACEHOLDER } from "react-native-gifted-chat/lib/Constant";
import Color from "react-native-gifted-chat/lib/Color";
const styles = StyleSheet.create({
  textInput: {
    flex: undefined,
    lineHeight: 22,
    paddingTop: 0,
    paddingBottom: 0,
    paddingLeft: 0,
    borderRadius: 0,
    borderWidth: 0,
    backgroundColor: "transparent",
    borderColor: "transparent",
    paddingRight: 0,
    margin: 0,
    marginLeft: 0,
    marginTop: 0,
    marginRight: 0,
    marginBottom: 0,
    minHeight: 55,
    height: 106,
    maxHeight: 106,
    textAlignVertical: "top",
    width: "100%",
    justifyContent: "flex-start",
    alignItems: "flex-start",
    fontSize: 16,
  },
});
const CustomLayoutSpring = {
  duration: 200,
  create: {
    type: LayoutAnimation.Types.easeOut,
    property: LayoutAnimation.Properties.opacity,
    springDamping: 0.7,
  },
  update: {
    type: LayoutAnimation.Types.easeOut,
    springDamping: 0.7,
  },
  delete: {
    type: LayoutAnimation.Types.easeOut,
    property: LayoutAnimation.Properties.opacity,
    springDamping: 0.7,
  },
};
export default class Composer extends React.PureComponent {
  state = {
    finalInputHeight: 0,
  };

  inputRef = React.createRef();

  constructor() {
    super(...arguments);
    if (Platform.OS === "android") {
      UIManager.setLayoutAnimationEnabledExperimental &&
        UIManager.setLayoutAnimationEnabledExperimental(true);
    }

    this.contentSize = undefined;
    this.onContentSizeChange = (e) => {
      const { contentSize } = e.nativeEvent;
      // Support earlier versions of React Native on Android.
      if (!contentSize) {
        return;
      }
      if (
        !this.contentSize ||
        (this.contentSize && this.contentSize.height !== contentSize.height)
      ) {
        this.contentSize = contentSize;
        if (!this.props.text.length) {
          LayoutAnimation.configureNext(CustomLayoutSpring);
          this.setState({ finalInputHeight: 0 });
          this.props.onInputSizeChanged({ width: 0, height: 0 });
        } else {
          this.calcInputHeight();
          this.props.onInputSizeChanged(this.contentSize);
        }
      }
    };
    this.onChangeText = (text) => {
      if (text.length < 2) {
        LayoutAnimation.configureNext(CustomLayoutSpring);
      }
      this.props.onTextChanged(text);
    };
    this.calcInputHeight = () => {
      if (this.contentSize && this.contentSize.height) {
        if (!this.props.text.length && this.state?.finalInputHeight > 0) {
          LayoutAnimation.configureNext(CustomLayoutSpring);
          this.setState({
            finalInputHeight: 0,
          });
          return;
        }
        let height = this.contentSize.height;
        LayoutAnimation.configureNext(CustomLayoutSpring);
        this.setState({
          finalInputHeight: height + 14,
        });
      }
    };
  }
  render() {
    return (
      <View
        style={{
          flex: 1,
          position: "relative",
          justifyContent: "flex-start",
          borderRadius: 20,
          overflow: "hidden",
          backgroundColor: "#f5f5f5",
          marginLeft: 10,
          marginRight: 10,
          paddingTop: 6,
          paddingBottom: 0,
          paddingLeft: 12,
          paddingRight: 12,
          borderWidth: 0.5,
          borderColor: "#b7cc23",
          marginTop: this.state.finalInputHeight > 44 ? 3 : 6,
          minHeight: 38,
          maxHeight: 118,
          height: this.state.finalInputHeight,
        }}
      >
        <TextInput
          testID={this.props.placeholder}
          accessible
          accessibilityLabel={this.props.placeholder}
          placeholder={this.props.placeholder}
          placeholderTextColor={this.props.placeholderTextColor}
          multiline={this.props.multiline}
          editable={!this.props.disableComposer}
          onContentSizeChange={this.onContentSizeChange}
          onChangeText={this.onChangeText}
          textBreakStrategy="highQuality"
          style={styles.textInput}
          autoFocus={this.props.textInputAutoFocus}
          value={this.props.text}
          autoCompleteType="off"
          enablesReturnKeyAutomatically
          underlineColorAndroid="transparent"
          keyboardAppearance={this.props.keyboardAppearance}
          {...this.props.textInputProps}
          ref={this.inputRef}
        />
      </View>
    );
  }
}
Composer.defaultProps = {
  composerHeight: MIN_COMPOSER_HEIGHT,
  text: "",
  placeholderTextColor: Color.defaultColor,
  placeholder: DEFAULT_PLACEHOLDER,
  textInputProps: null,
  multiline: true,
  disableComposer: false,
  textInputStyle: {},
  textInputAutoFocus: false,
  keyboardAppearance: "default",
  onTextChanged: () => {},
  onInputSizeChanged: () => {},
};
Composer.propTypes = {
  composerHeight: PropTypes.number,
  text: PropTypes.string,
  placeholder: PropTypes.string,
  placeholderTextColor: PropTypes.string,
  textInputProps: PropTypes.object,
  onTextChanged: PropTypes.func,
  onInputSizeChanged: PropTypes.func,
  multiline: PropTypes.bool,
  disableComposer: PropTypes.bool,
  textInputStyle: PropTypes.any,
  textInputAutoFocus: PropTypes.bool,
  keyboardAppearance: PropTypes.string,
};

Other Answers:

@izakfilmalter functional component version πŸ˜‰

really god job @Hirbod

import React, {FC, useState, useRef} from 'react';
import {
  StyleSheet,
  View,
  LayoutAnimation,
  TextInput,
  UIManager,
} from 'react-native';
import {DEFAULT_PLACEHOLDER} from 'react-native-gifted-chat/lib/Constant';
import {ComposerProps} from 'react-native-gifted-chat';

import {isAndroid} from '../../utils/constants';
import {COLORS} from '../../utils/styles';

interface ContentSize {
  width: number;
  height: number;
}

type NativeElement = {
  nativeEvent: {
    contentSize: ContentSize;
  };
};

const CustomLayoutSpring = {
  duration: 200,
  create: {
    type: LayoutAnimation.Types.easeOut,
    property: LayoutAnimation.Properties.opacity,
    springDamping: 0.7,
  },
  update: {
    type: LayoutAnimation.Types.easeOut,
    springDamping: 0.7,
  },
  delete: {
    type: LayoutAnimation.Types.easeOut,
    property: LayoutAnimation.Properties.opacity,
    springDamping: 0.7,
  },
};

const ChatComposer: FC<ComposerProps> = ({
  text = '',
  placeholder = DEFAULT_PLACEHOLDER,
  placeholderTextColor,
  textInputProps,
  onTextChanged,
  onInputSizeChanged,
  multiline = true,
  disableComposer = false,
  textInputAutoFocus,
  keyboardAppearance,
}) => {
  if (isAndroid) {
    UIManager.setLayoutAnimationEnabledExperimental &&
      UIManager.setLayoutAnimationEnabledExperimental(true);
  }
  const inputRef: any = useRef(null);
  const [newContentSize, setNewContentSize] = useState<
    ContentSize | undefined
  >();
  const [finalInputHeight, setFinalInputHeight] = useState(28);

  const calcInputHeight = (contentSize: ContentSize) => {
    if (contentSize?.height) {
      if (!text?.length && finalInputHeight) {
        LayoutAnimation.configureNext(CustomLayoutSpring);
        setFinalInputHeight(0);

        return;
      }
      LayoutAnimation.configureNext(CustomLayoutSpring);
      setFinalInputHeight(contentSize.height + 14);
    }
  };

  const onContentSizeChange = ({nativeEvent: {contentSize}}: NativeElement) => {
    if (!contentSize) {
      return;
    }
    if (
      !newContentSize ||
      (newContentSize && newContentSize.height !== contentSize.height)
    ) {
      setNewContentSize(contentSize);
      if (!text?.length && onInputSizeChanged) {
        LayoutAnimation.configureNext(CustomLayoutSpring);
        setFinalInputHeight(0);
        onInputSizeChanged({width: 0, height: 0});
      } else if (onInputSizeChanged) {
        calcInputHeight(contentSize);
        onInputSizeChanged(contentSize);
      }
    }
  };

  const onChangeText = (text: string) => {
    if (text.length < 2) {
      LayoutAnimation.configureNext(CustomLayoutSpring);
    }
    onTextChanged && onTextChanged(text);
  };

  return (
    <View
      style={[
        styles.composer,
        {marginTop: finalInputHeight > 44 ? 3 : 6, height: finalInputHeight},
      ]}>
      <TextInput
        ref={inputRef}
        testID={placeholder}
        accessible
        accessibilityLabel={placeholder}
        placeholder={placeholder}
        placeholderTextColor={placeholderTextColor}
        multiline={multiline}
        editable={!disableComposer}
        onContentSizeChange={onContentSizeChange}
        onChangeText={onChangeText}
        textBreakStrategy="highQuality"
        style={styles.textInput}
        autoFocus={textInputAutoFocus}
        value={text}
        autoCompleteType="off"
        enablesReturnKeyAutomatically
        underlineColorAndroid="transparent"
        keyboardAppearance={keyboardAppearance}
        {...textInputProps}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  composer: {
    flex: 1,
    position: 'relative',
    justifyContent: 'flex-start',
    borderRadius: 20,
    overflow: 'hidden',
    backgroundColor: '#f5f5f5',
    marginLeft: 10,
    marginRight: 10,
    paddingTop: 6,
    paddingBottom: 0,
    paddingLeft: 12,
    paddingRight: 12,
    borderWidth: 0.5,
    borderColor: '#b7cc23',
    minHeight: 38,
    maxHeight: 118,
  },
  textInput: {
    flex: undefined,
    lineHeight: 22,
    paddingTop: 0,
    paddingBottom: 0,
    paddingLeft: 0,
    borderRadius: 0,
    borderWidth: 0,
    backgroundColor: 'transparent',
    borderColor: 'transparent',
    paddingRight: 0,
    margin: 0,
    marginLeft: 0,
    marginTop: 0,
    marginRight: 0,
    marginBottom: 0,
    minHeight: 55,
    height: 106,
    maxHeight: 106,
    textAlignVertical: 'top',
    width: '100%',
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    fontSize: 16,
  },

export default ChatComposer;

@newkolay ok I fixed it:
Change the onContentSizeChange function with this here. Thanks for reporting, since it was also bugged in my App :-D

    this.onContentSizeChange = (e) => {
      const { contentSize } = e.nativeEvent;
      // Support earlier versions of React Native on Android.
      if (!contentSize) {
        return;
      }
      if (
        !this.contentSize ||
        (this.contentSize && this.contentSize.height !== contentSize.height)
      ) {
        this.contentSize = contentSize;
        if (!this.props.text.length) {
          LayoutAnimation.configureNext(CustomLayoutSpring);
          this.setState({ finalInputHeight: 0 });
          this.props.onInputSizeChanged({ width: 0, height: 0 });
        } else {
          this.calcInputHeight();
          this.props.onInputSizeChanged(this.contentSize);
        }
      }
    };

I also updated my original post

There are many factors to keep in mind @newkolay.
This are some props which kick into the calculation (remember, they are all suited to my layout and app and might differ from yours and need to be adjusted)

bottomOffset={this.isIphoneX() ? 22 : -12}
minComposerHeight={28}
maxComposerHeight={106}
minInputToolbarHeight={50}
renderInputToolbar={this.renderInputToolbar.bind(this)}
renderComposer={this.renderComposer}

I also have a custom renderInputToolbar prop.
These paddings kick in too.

  renderInputToolbar(props) {
    //Add the extra styles via containerStyle
    return (
      <InputToolbar
        {...props}
        containerStyle={{
          backgroundColor: this.props.theme.colors.surface,
          paddingTop: 6,
          paddingBottom: 12,
          borderTopColor: this.props.theme.colors.disabled,
        }}
      />
    );
  }

And think to pass the composer right

  renderComposer(props) {
    // where composer is my first post
    return <Composer {...props} />;
  }

Fit those parameters to your need. I am currently rewriting my Chat to functional components and will do a big cleanup and repost my new effort in couple of weeks but for now it should be understandable. Make sure to also install the newest version since it has some toolbar fixes wich I've send a PR for.

I also think that a single iPhoneX check might not be enough since there are also android devices with a notch. I will change that to use safe-area insets once I'm done refactoring.

Related Issues:

113
react native gifted chat Different Bubble color for each user.
FYI this is the code example you want: Issue Description If more than 2 user in a conversation ...
62
react native gifted chat Latest React Native 0.62.0: Type Errror: Super expression must either be null or a function
I hacked together a fork w/ the new action-sheet using the currently released version here ...
49
react native gifted chat Text box not visible when keyboard active using expo
I got it. Just do like this. need to declare flex to parent view to make it works πŸ‘ ...
41
react native gifted chat How to use with Expo?
In the meantime I'm using the following workaround of using a keyboard spacer Trying to understand h...
18
react native gifted chat Multiline does not grow TextInput until several characters into new line
This was not an easy one It took me a lot of work to find and fix the issue too + animate the input ...
15
react native gifted chat Show/Hide/Disable chat textinput ?
This code works: renderInputToolbar={disable ? () => null : undefined} is there any function I can u...
10
react native gifted chat android:windowSoftInputMode="adjustResize" causes different parts of the app to adjust size
@mrnahidtalukder a workaround for now is to use another package and set the softinputmode programati...
248
yakyak Hangs on connecting state because of parsing error
Looks like Google added a nonce to a script tag which broke yakyak's HTML parsing ...
74
hangups Error: invalid_scope from oauth2
Got a workaround here! Using one of the urls linked above you can get to a programmatic_auth url tha...
63
Rocket.Chat AppArmor errors after Snap Update
So I have a workaround for this: nano /var/lib/snapd/apparmor/profiles/snap.rocketchat-server.rocket...
63
client How to uninstall the OSX client?
To uninstall keybase and KBFS: and then remove /Applications/Keybase.app On older versions you may n...
31
hangups Support for Google provided email accounts (i.e. myusername@myuniversity.edu)
Manual login process: Download and run this Python script It requires hangups to be installed Open t...
26
thelounge A logo for the project
Hey everyone! As I'm writing the changelog entry for the incoming release of v2.7.0 in which I am ma...
21
Rocket.Chat Can't access RocketChat after setting iframe URL to localhost
Hi a quick way to resolve getting back to the admin console via iframe : Go to iframe browser consol...
19
Rocket.Chat Franz - invalid URL since 0.52.0
So . there is an update workaround without the need of older server: Install Franz and quit it (ot j...
19
yakyak Crash at launch
I made this change to line 36 instead ({conversation event = []} = conv); When launching 1.5.9 ...
18
client How to uninstall on ubuntu?
IMHO this is a seriously bad design How come there is no option or preferences to config startup beh...
17
Rocket.Chat [BUG] Users can't send messages (>= 2.4.x)
Same issue here Worked around it by using the reset feature in /admin/Message Description: I updated...
15
Rocket.Chat Rocket.Chat stops working with 1000 active users
Thanks for all of your suggestions We could now prove that the UserPresenceMonitor was responsible f...
15
Rocket.Chat How can I move rocket chat to other server?
@karamata do a mongo export: tar it up copy it over to new host Your Rocket.Chat version: Rocket.Cha...
13
BotFramework WebChat showing special character (Ò€ℒ) instead of β€˜ (apostrophe) character
I can reproduce this reliably for things like three dots in a row (...) on Safari and Chrome on OSX ...
12
yakyak YakYak stopped working with Hangouts
I can confirm that changing the email field number in YakYak-darwin-x64/YakYak.app/Contents/Resource...
6
Rocket.Chat.Electron New v3 version doesn't connect with a valid server
Reverting to v2.17.11 and works fine. My Setup Operating System: macOS Catalina v10.15.6 App Version...
5
Rocket.Chat Atlassian Crowd integration doesn't work after upgrading Atllasian Crowd from 3.7.1 to 4.0.0
@karl-in-office @flover97 @gabriellsh (@gammpamm @fbuchmeier Description: Our Rocket.Chat server wor...
5
Rocket.Chat Can't connect to custom deployed Rocket.Chat using mobile apps (iOS or Android)
Somehow mine started working only change was to delete and reinstall the mobile app.. For reference ...
5
Rocket.Chat Migration Issues after Update Release Version
That is a really bad habit to use tag latest with docker deployment in general and with rocketchat p...
5
Rocket.Chat File upload doesn't work
@gregharvey I have the same problem File Upload is working if I run Rocket.chat as root but this is ...
3
Rocket.Chat Fail to start rocket.chat snap
@mholt thanks for taking the time to post on this issue I'll get another build of this going to give...
3
Rocket.Chat RealTime API stream-notify-user/message event
Seems like I found it The collection name is stream-room-messages and event name is __my_messages__ ...
3
react virtuoso v1 beta is available - test now if you are building chat / feed interfaces
Huge thank you everyone for the feedback the contributions and the testing v1 is now official and pu...
3
ejabberd XEP-0359: stanza-id in each of received message
This is what we need to do to announce urn:xmpp:sid:0 and urn:xmpp:mam:2 support ...
442
react swipeable views lazy loading is useless right now
Normally React wants to keep the UI consistent with what you told it to render Lazy loading should m...
78
vue multiselect Id from object
What worked for me with an array of objects like: let types = [ {id: 1 name: john} {id: 2 name: stev...
28
vue multiselect Add bootstrap style for vue-multiselect component.
Here is the SASS file I am using This is for Bootstrap 4 and tries to reuse as much as BS variables ...
24
vuetable 2 Unknown plugin "transform-runtime"
@Shhu I still haven't try Laravel Mix so I cannot say for sure But from the error message it looks l...
24
create react library Error when using hooks
Just found this today after a little more digging So the React team is aware of it ...
23
google map react Zooming is animating markers from the corners
@VladimirMilov try to add v: '3.30' into bootstrapURLKeys https://developers.google.com/maps/documen...
19
vue multiselect No ability to infinite scroll
I managed to have infinite scroll without a button by adding vue-observe-visibility to the afterList...
17
emoji mart How replace emoji codes in html to Emoji Component?
πŸ‘‹ You will need a regex to detect the colons-syntax emojis Hello ...
17
react frame component Adjust height of iframe based on the height of content inside
I managed to get something working As the content inside the iframe keeps changing i want the height...
13
google map react Generated child component not capturing click
If you want to suppress map clicks you can add listeners on your component that call e.stopPropagati...
11
Vue.Draggable Sometimes stops working in Firefox
adding :options={forceFallback: true} made it work again in my case I'm using the latest Firefox Dev...
11
react sortable tree Interacting With Nodes
That's a perfectly good question I just tried this in my environment and it seemed to work: ...
10
vuesax tooltip don't work
I have also discovered that tooltip only works if it contains two child nodes Following Snippet work...
10
vue multiselect remove autocomplete in chrome browser
I have resolved this issue by using refs Below is the code which i have written in mounted life hook...
9
react native modalize [react-native-modalize] swipeable not working on android
Thanks @JonnyBurger for the dive in I think in the early version of the package ...
6
react swipeable views Circular slides?
Here is an example of implementation with 3 different slides: This API has some pros and cons Pros R...
5
vuesax Setting theme doesn't work with SSR
Thank you very much I will take this as the next problem to solve Hello I just installed a clean nux...
3
react rnd Is it necessary to set the parent elements position?
Will you also remove the isMounted logic (as its not really needed I think) and this line: if (!this...