diff --git a/assets/images/collapse.svg b/assets/images/collapse.svg
new file mode 100644
index 0000000000000..92b9619924f06
--- /dev/null
+++ b/assets/images/collapse.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/assets/images/expand.svg b/assets/images/expand.svg
new file mode 100644
index 0000000000000..cdd1d712fd6a6
--- /dev/null
+++ b/assets/images/expand.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/CONST.js b/src/CONST.js
index 5b1b82da59048..124369fc19ea7 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -273,6 +273,14 @@ const CONST = {
MAX_ROOM_NAME_LENGTH: 80,
LAST_MESSAGE_TEXT_MAX_LENGTH: 80,
},
+ COMPOSER: {
+ MAX_LINES: 16,
+ MAX_LINES_SMALL_SCREEN: 6,
+ MAX_LINES_FULL: -1,
+
+ // The minimum number of typed lines needed to enable the full screen composer
+ FULL_COMPOSER_MIN_LINES: 3,
+ },
MODAL: {
MODAL_TYPE: {
CONFIRM: 'confirm',
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 9e198d07aad7e..1f47b414ee230 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -105,6 +105,7 @@ export default {
REPORT_IOUS: 'reportIOUs_',
POLICY: 'policy_',
REPORTS_WITH_DRAFT: 'reportWithDraft_',
+ REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
},
// Indicates which locale should be used
diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js
index 687ac17ff91b9..76b63eb7f972a 100644
--- a/src/components/Composer/index.android.js
+++ b/src/components/Composer/index.android.js
@@ -1,16 +1,11 @@
import React from 'react';
+import {StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import RNTextInput from '../RNTextInput';
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
-
-/**
- * On native layers we like to have the Text Input not focused so the user can read new chats without they keyboard in
- * the way of the view
- * On Android, the selection prop is required on the TextInput but this prop has issues on IOS
- * https://github.com/facebook/react-native/issues/29063
- */
+import * as ComposerUtils from '../../libs/ComposerUtils';
const propTypes = {
/** If the input should clear, it actually gets intercepted instead of .clear() */
@@ -29,6 +24,25 @@ const propTypes = {
/** Prevent edits and interactions like focus for this input. */
isDisabled: PropTypes.bool,
+ /** Selection Object */
+ selection: PropTypes.shape({
+ start: PropTypes.number,
+ end: PropTypes.number,
+ }),
+
+ /** Whether the full composer can be opened */
+ isFullComposerAvailable: PropTypes.bool,
+
+ /** Allow the full composer to be opened */
+ setIsFullComposerAvailable: PropTypes.func,
+
+ /** Whether the composer is full size */
+ isComposerFullSize: PropTypes.bool.isRequired,
+
+ /** General styles to apply to the text input */
+ // eslint-disable-next-line react/forbid-prop-types
+ style: PropTypes.any,
+
};
const defaultProps = {
@@ -37,9 +51,24 @@ const defaultProps = {
autoFocus: false,
isDisabled: false,
forwardedRef: null,
+ selection: {
+ start: 0,
+ end: 0,
+ },
+ isFullComposerAvailable: false,
+ setIsFullComposerAvailable: () => {},
+ style: null,
};
class Composer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ propStyles: StyleSheet.flatten(this.props.style),
+ };
+ }
+
componentDidMount() {
// This callback prop is used by the parent component using the constructor to
// get a ref to the inner textInput element e.g. if we do
@@ -67,17 +96,19 @@ class Composer extends React.Component {
autoComplete="off"
placeholderTextColor={themeColors.placeholderText}
ref={el => this.textInput = el}
- maxHeight={CONST.COMPOSER_MAX_HEIGHT}
+ maxHeight={this.props.isComposerFullSize ? '100%' : CONST.COMPOSER_MAX_HEIGHT}
+ onContentSizeChange={e => ComposerUtils.updateNumberOfLines(this.props, e)}
rejectResponderTermination={false}
- editable={!this.props.isDisabled}
+ textAlignVertical="center"
+ style={this.state.propStyles}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...this.props}
+ editable={!this.props.isDisabled}
/>
);
}
}
-Composer.displayName = 'Composer';
Composer.propTypes = propTypes;
Composer.defaultProps = defaultProps;
diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js
index d61339f18f3ff..9b1e6680dea3f 100644
--- a/src/components/Composer/index.ios.js
+++ b/src/components/Composer/index.ios.js
@@ -1,16 +1,11 @@
import React from 'react';
+import {StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import RNTextInput from '../RNTextInput';
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
-
-/**
- * On native layers we like to have the Text Input not focused so the user can read new chats without they keyboard in
- * the way of the view
- * On Android, the selection prop is required on the TextInput but this prop has issues on IOS
- * https://github.com/facebook/react-native/issues/29063
- */
+import * as ComposerUtils from '../../libs/ComposerUtils';
const propTypes = {
/** If the input should clear, it actually gets intercepted instead of .clear() */
@@ -35,6 +30,19 @@ const propTypes = {
end: PropTypes.number,
}),
+ /** Whether the full composer can be opened */
+ isFullComposerAvailable: PropTypes.bool,
+
+ /** Allow the full composer to be opened */
+ setIsFullComposerAvailable: PropTypes.func,
+
+ /** Whether the composer is full size */
+ isComposerFullSize: PropTypes.bool.isRequired,
+
+ /** General styles to apply to the text input */
+ // eslint-disable-next-line react/forbid-prop-types
+ style: PropTypes.any,
+
};
const defaultProps = {
@@ -47,9 +55,20 @@ const defaultProps = {
start: 0,
end: 0,
},
+ isFullComposerAvailable: false,
+ setIsFullComposerAvailable: () => {},
+ style: null,
};
class Composer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ propStyles: StyleSheet.flatten(this.props.style),
+ };
+ }
+
componentDidMount() {
// This callback prop is used by the parent component using the constructor to
// get a ref to the inner textInput element e.g. if we do
@@ -72,18 +91,24 @@ class Composer extends React.Component {
}
render() {
- // Selection Property not worked in IOS properly, So removed from props.
+ // On native layers we like to have the Text Input not focused so the
+ // user can read new chats without the keyboard in the way of the view.
+ // On Android, the selection prop is required on the TextInput but this prop has issues on IOS
+ // https://github.com/facebook/react-native/issues/29063
const propsToPass = _.omit(this.props, 'selection');
return (
this.textInput = el}
- maxHeight={CONST.COMPOSER_MAX_HEIGHT}
+ maxHeight={this.props.isComposerFullSize ? '100%' : CONST.COMPOSER_MAX_HEIGHT}
+ onContentSizeChange={e => ComposerUtils.updateNumberOfLines(this.props, e)}
rejectResponderTermination={false}
- editable={!this.props.isDisabled}
+ textAlignVertical="center"
+ style={this.state.propStyles}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...propsToPass}
+ editable={!this.props.isDisabled}
/>
);
}
diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js
index 740d28757a6f3..3d0b4f351f7c3 100755
--- a/src/components/Composer/index.js
+++ b/src/components/Composer/index.js
@@ -8,6 +8,8 @@ import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import Growl from '../../libs/Growl';
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
+import updateIsFullComposerAvailable from '../../libs/ComposerUtils/updateIsFullComposerAvailable';
+import getNumberOfLines from '../../libs/ComposerUtils/index';
const propTypes = {
/** Maximum number of lines in the text input */
@@ -63,6 +65,12 @@ const propTypes = {
end: PropTypes.number,
}),
+ /** Whether the full composer can be opened */
+ isFullComposerAvailable: PropTypes.bool,
+
+ /** Allow the full composer to be opened */
+ setIsFullComposerAvailable: PropTypes.func,
+
...withLocalizePropTypes,
};
@@ -86,6 +94,8 @@ const defaultProps = {
start: 0,
end: 0,
},
+ isFullComposerAvailable: false,
+ setIsFullComposerAvailable: () => {},
};
const IMAGE_EXTENSIONS = {
@@ -155,7 +165,8 @@ class Composer extends React.Component {
this.setState({numberOfLines: 1});
this.props.onClear();
}
- if (prevProps.defaultValue !== this.props.defaultValue) {
+ if (prevProps.defaultValue !== this.props.defaultValue
+ || prevProps.isComposerFullSize !== this.props.isComposerFullSize) {
this.updateNumberOfLines();
}
@@ -178,22 +189,6 @@ class Composer extends React.Component {
this.textInput.removeEventListener('wheel', this.handleWheel);
}
- /**
- * Calculates the max number of lines the text input can have
- *
- * @param {Number} lineHeight
- * @param {Number} paddingTopAndBottom
- * @param {Number} scrollHeight
- *
- * @returns {Number}
- */
- getNumberOfLines(lineHeight, paddingTopAndBottom, scrollHeight) {
- const maxLines = this.props.maxLines;
- let newNumberOfLines = Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight);
- newNumberOfLines = maxLines <= 0 ? newNumberOfLines : Math.min(newNumberOfLines, maxLines);
- return newNumberOfLines;
- }
-
/**
* Handles all types of drag-N-drop events on the composer
*
@@ -328,16 +323,21 @@ class Composer extends React.Component {
* divide by line height to get the total number of rows for the textarea.
*/
updateNumberOfLines() {
- const computedStyle = window.getComputedStyle(this.textInput);
- const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20;
- const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10)
- + parseInt(computedStyle.paddingTop, 10);
+ // Hide the composer expand button so we can get an accurate reading of
+ // the height of the text input
+ this.props.setIsFullComposerAvailable(false);
// We have to reset the rows back to the minimum before updating so that the scrollHeight is not
// affected by the previous row setting. If we don't, rows will be added but not removed on backspace/delete.
this.setState({numberOfLines: 1}, () => {
+ const computedStyle = window.getComputedStyle(this.textInput);
+ const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20;
+ const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10)
+ + parseInt(computedStyle.paddingTop, 10);
+ const numberOfLines = getNumberOfLines(this.props.maxLines, lineHeight, paddingTopAndBottom, this.textInput.scrollHeight);
+ updateIsFullComposerAvailable(this.props, numberOfLines);
this.setState({
- numberOfLines: this.getNumberOfLines(lineHeight, paddingTopAndBottom, this.textInput.scrollHeight),
+ numberOfLines,
});
});
}
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index 0e8d598ad703a..0b11a5602b131 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -16,6 +16,7 @@ import CircleHourglass from '../../../assets/images/circle-hourglass.svg';
import Clipboard from '../../../assets/images/clipboard.svg';
import Close from '../../../assets/images/close.svg';
import ClosedSign from '../../../assets/images/closed-sign.svg';
+import Collapse from '../../../assets/images/collapse.svg';
import Concierge from '../../../assets/images/concierge.svg';
import CreditCard from '../../../assets/images/creditcard.svg';
import DownArrow from '../../../assets/images/down.svg';
@@ -23,6 +24,7 @@ import Download from '../../../assets/images/download.svg';
import Emoji from '../../../assets/images/emoji.svg';
import Exclamation from '../../../assets/images/exclamation.svg';
import Exit from '../../../assets/images/exit.svg';
+import Expand from '../../../assets/images/expand.svg';
import Eye from '../../../assets/images/eye.svg';
import EyeDisabled from '../../../assets/images/eye-disabled.svg';
import ExpensifyCard from '../../../assets/images/expensifycard.svg';
@@ -102,6 +104,7 @@ export {
Clipboard,
Close,
ClosedSign,
+ Collapse,
Concierge,
Connect,
CreditCard,
@@ -112,6 +115,7 @@ export {
Emoji,
Exclamation,
Exit,
+ Expand,
Eye,
EyeDisabled,
ExpensifyCard,
diff --git a/src/languages/en.js b/src/languages/en.js
index ec3b1554a9a98..0bc2f39406660 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -164,6 +164,8 @@ export default {
localTime: ({user, time}) => `It's ${time} for ${user}`,
edited: '(edited)',
emoji: 'Emoji',
+ collapse: 'Collapse',
+ expand: 'Expand',
},
reportActionContextMenu: {
copyToClipboard: 'Copy to clipboard',
diff --git a/src/languages/es.js b/src/languages/es.js
index b2c86f47e3108..6e21739200c30 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -164,6 +164,8 @@ export default {
localTime: ({user, time}) => `Son las ${time} para ${user}`,
edited: '(editado)',
emoji: 'Emoji',
+ collapse: 'Colapsar',
+ expand: 'Expandir',
},
reportActionContextMenu: {
copyToClipboard: 'Copiar al portapapeles',
diff --git a/src/libs/ComposerUtils/index.js b/src/libs/ComposerUtils/index.js
new file mode 100644
index 0000000000000..a469da7516bbc
--- /dev/null
+++ b/src/libs/ComposerUtils/index.js
@@ -0,0 +1,17 @@
+/**
+ * Get the current number of lines in the composer
+ *
+ * @param {Number} maxLines
+ * @param {Number} lineHeight
+ * @param {Number} paddingTopAndBottom
+ * @param {Number} scrollHeight
+ *
+ * @returns {Number}
+ */
+function getNumberOfLines(maxLines, lineHeight, paddingTopAndBottom, scrollHeight) {
+ let newNumberOfLines = Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight);
+ newNumberOfLines = maxLines <= 0 ? newNumberOfLines : Math.min(newNumberOfLines, maxLines);
+ return newNumberOfLines;
+}
+
+export default getNumberOfLines;
diff --git a/src/libs/ComposerUtils/index.native.js b/src/libs/ComposerUtils/index.native.js
new file mode 100644
index 0000000000000..783fbac9a4269
--- /dev/null
+++ b/src/libs/ComposerUtils/index.native.js
@@ -0,0 +1,38 @@
+import lodashGet from 'lodash/get';
+import styles from '../../styles/styles';
+import updateIsFullComposerAvailable from './updateIsFullComposerAvailable';
+
+/**
+ * Get the current number of lines in the composer
+ *
+ * @param {Number} lineHeight
+ * @param {Number} paddingTopAndBottom
+ * @param {Number} scrollHeight
+ *
+ * @returns {Number}
+ */
+function getNumberOfLines(lineHeight, paddingTopAndBottom, scrollHeight) {
+ return Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight);
+}
+
+/**
+ * Check the current scrollHeight of the textarea (minus any padding) and
+ * divide by line height to get the total number of rows for the textarea.
+ * @param {Object} props
+ * @param {Event} e
+ */
+function updateNumberOfLines(props, e) {
+ const lineHeight = styles.textInputCompose.lineHeight;
+ const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2;
+ const inputHeight = lodashGet(e, 'nativeEvent.contentSize.height', null);
+ if (!inputHeight) {
+ return;
+ }
+ const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight);
+ updateIsFullComposerAvailable(props, numberOfLines);
+}
+
+export {
+ getNumberOfLines,
+ updateNumberOfLines,
+};
diff --git a/src/libs/ComposerUtils/updateIsFullComposerAvailable.js b/src/libs/ComposerUtils/updateIsFullComposerAvailable.js
new file mode 100644
index 0000000000000..00b12d1742e3e
--- /dev/null
+++ b/src/libs/ComposerUtils/updateIsFullComposerAvailable.js
@@ -0,0 +1,15 @@
+import CONST from '../../CONST';
+
+/**
+ * Update isFullComposerAvailable if needed
+ * @param {Object} props
+ * @param {Number} numberOfLines The number of lines in the text input
+ */
+function updateIsFullComposerAvailable(props, numberOfLines) {
+ const isFullComposerAvailable = numberOfLines >= CONST.COMPOSER.FULL_COMPOSER_MIN_LINES;
+ if (isFullComposerAvailable !== props.isFullComposerAvailable) {
+ props.setIsFullComposerAvailable(isFullComposerAvailable);
+ }
+}
+
+export default updateIsFullComposerAvailable;
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 3abd3ba123568..618d38bcf824f 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1414,6 +1414,14 @@ function renameReport(reportID, reportName) {
.finally(() => Onyx.set(ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM, false));
}
+/**
+ * @param {Number} reportID
+ * @param {Boolean} isComposerFullSize
+ */
+function setIsComposerFullSize(reportID, isComposerFullSize) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, isComposerFullSize);
+}
+
/**
* @param {Number} reportID
* @param {Object} action
@@ -1551,4 +1559,5 @@ export {
createPolicyRoom,
renameReport,
getLastReadSequenceNumber,
+ setIsComposerFullSize,
};
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index d03dca98fcd53..5d32a341c7256 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -2,6 +2,7 @@ import React from 'react';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import {Keyboard, View} from 'react-native';
+import lodashGet from 'lodash/get';
import _ from 'underscore';
import lodashFindLast from 'lodash/findLast';
import styles from '../../styles/styles';
@@ -15,7 +16,7 @@ import Permissions from '../../libs/Permissions';
import * as ReportUtils from '../../libs/ReportUtils';
import ReportActionsView from './report/ReportActionsView';
import ReportActionCompose from './report/ReportActionCompose';
-import KeyboardSpacer from '../../components/KeyboardSpacer';
+import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
import SwipeableView from '../../components/SwipeableView';
import CONST from '../../CONST';
import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator';
@@ -59,6 +60,9 @@ const propTypes = {
/** Array of report actions for this report */
reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+ /** Whether the composer is full size */
+ isComposerFullSize: PropTypes.bool,
+
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
};
@@ -74,6 +78,7 @@ const defaultProps = {
maxSequenceNumber: 0,
hasOutstandingIOU: false,
},
+ isComposerFullSize: false,
betas: [],
};
@@ -95,15 +100,20 @@ class ReportScreen extends React.Component {
super(props);
this.onSubmitComment = this.onSubmitComment.bind(this);
+ this.viewportOffsetTop = this.updateViewportOffsetTop.bind(this);
this.state = {
isLoading: true,
+ viewportOffsetTop: 0,
};
}
componentDidMount() {
this.prepareTransition();
this.storeCurrentlyViewedReport();
+ if (window.visualViewport) {
+ window.visualViewport.addEventListener('resize', this.viewportOffsetTop);
+ }
}
componentDidUpdate(prevProps) {
@@ -117,6 +127,9 @@ class ReportScreen extends React.Component {
componentWillUnmount() {
clearTimeout(this.loadingTimerId);
+ if (window.visualViewport) {
+ window.visualViewport.removeEventListener('resize', this.viewportOffsetTop);
+ }
}
/**
@@ -126,6 +139,14 @@ class ReportScreen extends React.Component {
Report.addAction(getReportID(this.props.route), text);
}
+ /**
+ * @param {SyntheticEvent} e
+ */
+ updateViewportOffsetTop(e) {
+ const viewportOffsetTop = lodashGet(e, 'target.offsetTop', 0);
+ this.setState({viewportOffsetTop});
+ }
+
/**
* When reports change there's a brief time content is not ready to be displayed
*
@@ -181,27 +202,29 @@ class ReportScreen extends React.Component {
}
return (
-
- Navigation.navigate(ROUTES.HOME)}
- />
-
-
- {this.shouldShowLoader() && }
- {!this.shouldShowLoader() && (
-
- )}
- {(isArchivedRoom || this.props.session.shouldShowComposeInput) && (
-
+
+
+ Navigation.navigate(ROUTES.HOME)}
+ />
+
+
+ {this.shouldShowLoader() && }
+ {!this.shouldShowLoader() && (
+
+ )}
+ {(isArchivedRoom || this.props.session.shouldShowComposeInput) && (
+
{
isArchivedRoom
? (
@@ -216,14 +239,15 @@ class ReportScreen extends React.Component {
reportID={reportID}
reportActions={this.props.reportActions}
report={this.props.report}
+ isComposerFullSize={this.props.isComposerFullSize}
/>
)
}
- )}
-
-
+ )}
+
+
);
}
@@ -246,6 +270,9 @@ export default withOnyx({
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`,
},
+ isComposerFullSize: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`,
+ },
betas: {
key: ONYXKEYS.BETAS,
},
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index d7ebdb896a051..a48ef025fa0ab 100755
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -87,6 +87,9 @@ const propTypes = {
/** Is composer screen focused */
isFocused: PropTypes.bool.isRequired,
+ /** Is the composer full size */
+ isComposerFullSize: PropTypes.bool.isRequired,
+
// The NVP describing a user's block status
blockedFromConcierge: PropTypes.shape({
// The date that the user will be unblocked
@@ -123,6 +126,7 @@ class ReportActionCompose extends React.Component {
this.triggerHotkeyActions = this.triggerHotkeyActions.bind(this);
this.submitForm = this.submitForm.bind(this);
this.setIsFocused = this.setIsFocused.bind(this);
+ this.setIsFullComposerAvailable = this.setIsFullComposerAvailable.bind(this);
this.focus = this.focus.bind(this);
this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this);
this.comment = props.comment;
@@ -134,6 +138,7 @@ class ReportActionCompose extends React.Component {
this.state = {
isFocused: this.shouldFocusInputOnScreenFocus,
+ isFullComposerAvailable: props.isComposerFullSize,
textInputShouldClear: false,
isCommentEmpty: props.comment.length === 0,
isMenuVisible: false,
@@ -141,6 +146,7 @@ class ReportActionCompose extends React.Component {
start: props.comment.length,
end: props.comment.length,
},
+ maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES,
};
}
@@ -152,6 +158,7 @@ class ReportActionCompose extends React.Component {
this.focus(false);
});
+ this.setMaxLines();
this.updateComment(this.comment);
}
@@ -169,6 +176,10 @@ class ReportActionCompose extends React.Component {
this.focus();
}
+ if (this.props.isComposerFullSize !== prevProps.isComposerFullSize) {
+ this.setMaxLines();
+ }
+
// As the report IDs change, make sure to update the composer comment as we need to make sure
// we do not show incorrect data in there (ie. draft of message from other report).
if (this.props.report.reportID === prevProps.report.reportID) {
@@ -195,6 +206,10 @@ class ReportActionCompose extends React.Component {
this.setState({isFocused: shouldHighlight});
}
+ setIsFullComposerAvailable(isFullComposerAvailable) {
+ this.setState({isFullComposerAvailable});
+ }
+
/**
* Updates the should clear state of the composer
*
@@ -280,6 +295,17 @@ class ReportActionCompose extends React.Component {
return iouOptions;
}
+ /**
+ * Set the maximum number of lines for the composer
+ */
+ setMaxLines() {
+ let maxLines = this.props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;
+ if (this.props.isComposerFullSize) {
+ maxLines = CONST.COMPOSER.MAX_LINES_FULL;
+ }
+ this.setState({maxLines});
+ }
+
/**
* Callback for the emoji picker to add whatever emoji is chosen into the main input
*
@@ -427,6 +453,10 @@ class ReportActionCompose extends React.Component {
this.props.onSubmit(trimmedComment);
this.updateComment('');
this.setTextInputShouldClear(true);
+ if (this.props.isComposerFullSize) {
+ Report.setIsComposerFullSize(this.props.reportID, false);
+ }
+ this.setState({isFullComposerAvailable: false});
// Important to reset the selection on Submit action
this.textInput.setNativeProps({selection: {start: 0, end: 0}});
@@ -440,7 +470,9 @@ class ReportActionCompose extends React.Component {
const reportParticipants = lodashGet(this.props.report, 'participants', []);
const reportRecipient = this.props.personalDetails[reportParticipants[0]];
- const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(this.props.personalDetails, this.props.report);
+
+ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(this.props.personalDetails, this.props.report)
+ && !this.props.isComposerFullSize;
// Prevents focusing and showing the keyboard while the drawer is covering the chat.
const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth;
@@ -449,15 +481,16 @@ class ReportActionCompose extends React.Component {
const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH;
return (
-
+
{shouldShowReportRecipientLocalTime
&& }
@@ -474,7 +507,42 @@ class ReportActionCompose extends React.Component {
{({openPicker}) => (
<>
-
+
+ {this.props.isComposerFullSize && (
+
+ {
+ e.preventDefault();
+ Report.setIsComposerFullSize(this.props.reportID, false);
+ }}
+ style={styles.composerSizeButton}
+ underlayColor={themeColors.componentBG}
+ disabled={isBlockedFromConcierge}
+ >
+
+
+
+
+ )}
+ {(!this.props.isComposerFullSize && this.state.isFullComposerAvailable) && (
+
+ {
+ e.preventDefault();
+ Report.setIsComposerFullSize(this.props.reportID, true);
+ }}
+ style={styles.composerSizeButton}
+ underlayColor={themeColors.componentBG}
+ disabled={isBlockedFromConcierge}
+ >
+
+
+
+ )}
{
@@ -511,53 +579,58 @@ class ReportActionCompose extends React.Component {
>
)}
- {
- if (!isOriginComposer) {
- return;
- }
-
- this.setState({isDraggingOver: true});
- }}
- onDragOver={(e, isOriginComposer) => {
- if (!isOriginComposer) {
- return;
- }
-
- this.setState({isDraggingOver: true});
- }}
- onDragLeave={() => this.setState({isDraggingOver: false})}
- onDrop={(e) => {
- e.preventDefault();
-
- const file = lodashGet(e, ['dataTransfer', 'files', 0]);
- if (!file) {
- return;
- }
-
- displayFileInModal({file});
- this.setState({isDraggingOver: false});
- }}
- style={[styles.textInputCompose, styles.flex4]}
- defaultValue={this.props.comment}
- maxLines={this.props.isSmallScreenWidth ? 6 : 16} // This is the same that slack has
- onFocus={() => this.setIsFocused(true)}
- onBlur={() => this.setIsFocused(false)}
- onPasteFile={file => displayFileInModal({file})}
- shouldClear={this.state.textInputShouldClear}
- onClear={() => this.setTextInputShouldClear(false)}
- isDisabled={isComposeDisabled || isBlockedFromConcierge}
- selection={this.state.selection}
- onSelectionChange={this.onSelectionChange}
- />
+
+ {
+ if (!isOriginComposer) {
+ return;
+ }
+
+ this.setState({isDraggingOver: true});
+ }}
+ onDragOver={(e, isOriginComposer) => {
+ if (!isOriginComposer) {
+ return;
+ }
+
+ this.setState({isDraggingOver: true});
+ }}
+ onDragLeave={() => this.setState({isDraggingOver: false})}
+ onDrop={(e) => {
+ e.preventDefault();
+
+ const file = lodashGet(e, ['dataTransfer', 'files', 0]);
+ if (!file) {
+ return;
+ }
+
+ displayFileInModal({file});
+ this.setState({isDraggingOver: false});
+ }}
+ style={[styles.textInputCompose, this.props.isComposerFullSize ? styles.textInputFullCompose : styles.flex4]}
+ defaultValue={this.props.comment}
+ maxLines={this.state.maxLines}
+ onFocus={() => this.setIsFocused(true)}
+ onBlur={() => this.setIsFocused(false)}
+ onPasteFile={file => displayFileInModal({file})}
+ shouldClear={this.state.textInputShouldClear}
+ onClear={() => this.setTextInputShouldClear(false)}
+ isDisabled={isComposeDisabled || isBlockedFromConcierge}
+ selection={this.state.selection}
+ onSelectionChange={this.onSelectionChange}
+ isFullComposerAvailable={this.state.isFullComposerAvailable}
+ setIsFullComposerAvailable={this.setIsFullComposerAvailable}
+ isComposerFullSize={this.props.isComposerFullSize}
+ />
+
>
)}
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index ef0520b143f9e..1e919028393f5 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -62,6 +62,9 @@ const propTypes = {
email: PropTypes.string,
}),
+ /** Whether the composer is full size */
+ isComposerFullSize: PropTypes.bool.isRequired,
+
/** Are we loading more report actions? */
isLoadingReportActions: PropTypes.bool,
@@ -189,6 +192,10 @@ class ReportActionsView extends React.Component {
return true;
}
+ if (this.props.isComposerFullSize !== nextProps.isComposerFullSize) {
+ return true;
+ }
+
return !_.isEqual(lodashGet(this.props.report, 'icons', []), lodashGet(nextProps.report, 'icons', []));
}
@@ -405,22 +412,26 @@ class ReportActionsView extends React.Component {
return (
<>
-
-
-
+ {!this.props.isComposerFullSize && (
+ <>
+
+
+
+ >
+ )}
>
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 58efa6b4cf733..493333605ff19 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1328,6 +1328,10 @@ const styles = {
minHeight: 90,
},
+ chatItemFullComposeRow: {
+ ...sizing.h100,
+ },
+
chatItemComposeBoxColor: {
borderColor: themeColors.border,
},
@@ -1343,6 +1347,12 @@ const styles = {
minHeight: variables.componentSizeNormal,
},
+ chatItemFullComposeBox: {
+ ...flex.flex1,
+ ...spacing.mt4,
+ ...sizing.h100,
+ },
+
chatFooter: {
minHeight: 65,
marginBottom: 5,
@@ -1352,6 +1362,12 @@ const styles = {
backgroundColor: themeColors.appBG,
},
+ chatFooterFullCompose: {
+ flex: 1,
+ flexShrink: 1,
+ flexBasis: '100%',
+ },
+
textInputCompose: addOutlineWidth({
backgroundColor: themeColors.componentBG,
borderColor: themeColors.border,
@@ -1368,12 +1384,23 @@ const styles = {
// paddingVertical: 0, alignSelf: 'center', and textAlignVertical: 'center'
paddingHorizontal: 8,
- marginVertical: 5,
paddingVertical: 0,
...textInputAlignSelf.center,
textAlignVertical: 'center',
}, 0),
+ textInputFullCompose: {
+ alignSelf: 'flex-end',
+ flex: 1,
+ maxHeight: '100%',
+ },
+
+ textInputComposeSpacing: {
+ paddingVertical: 6,
+ ...flex.flexRow,
+ flex: 1,
+ },
+
chatItemSubmitButton: {
alignSelf: 'flex-end',
borderRadius: 6,
@@ -1464,6 +1491,16 @@ const styles = {
width: 39,
},
+ composerSizeButton: {
+ alignItems: 'center',
+ alignSelf: 'flex-end',
+ height: 26,
+ marginBottom: 6,
+ marginTop: 6,
+ justifyContent: 'center',
+ width: 39,
+ },
+
chatItemAttachmentPlaceholder: {
backgroundColor: themeColors.sidebar,
borderColor: themeColors.border,