MM-22401 Refactor Post Draft (#4122)
* Babel TS and Eslint config * Channel is archived component * Move Typing component * Move Uploads component * Add PostInput component * QuickActions components * Add PostDraft component * Remove old PostTextbox component * Rename post_textbox constant * Fix Autocomplete * Rename blur post draft event * Fix references * Feedback review * Fix autocomplete for iOS
This commit is contained in:
parent
597e03a6bc
commit
1a605891fe
59 changed files with 1901 additions and 2585 deletions
|
|
@ -31,7 +31,7 @@ import EventEmitter from '@mm-redux/utils/event_emitter';
|
|||
|
||||
import {loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_textbox';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import telemetry from '@telemetry';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue} from '@utils/channels';
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default class AtMention extends PureComponent {
|
|||
}).isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
cursorPosition: PropTypes.number,
|
||||
defaultChannel: PropTypes.object,
|
||||
inChannel: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default class ChannelMention extends PureComponent {
|
|||
autocompleteChannelsForSearch: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
cursorPosition: PropTypes.number,
|
||||
isSearch: PropTypes.bool,
|
||||
matchTerm: PropTypes.string,
|
||||
maxListHeight: PropTypes.number,
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload';
|
||||
|
||||
import FileUploadItem from './file_upload_item';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handleRemoveFile,
|
||||
retryFileUpload,
|
||||
uploadComplete,
|
||||
uploadFailed,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FileUploadItem);
|
||||
102
app/components/post_draft/archived/archived.js
Normal file
102
app/components/post_draft/archived/archived.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import {popToRoot} from '@actions/navigation';
|
||||
import FormattedMarkdownText from '@components/formatted_markdown_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {t} from '@utils/i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class Archived extends PureComponent {
|
||||
static propTypes = {
|
||||
deactivated: PropTypes.bool,
|
||||
rootId: PropTypes.string,
|
||||
selectPenultimateChannel: PropTypes.func.isRequired,
|
||||
teamId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onCloseChannelPress = () => {
|
||||
const {rootId, teamId} = this.props;
|
||||
this.props.selectPenultimateChannel(teamId);
|
||||
if (rootId) {
|
||||
popToRoot();
|
||||
}
|
||||
};
|
||||
|
||||
message = () => {
|
||||
const {deactivated} = this.props;
|
||||
if (deactivated) {
|
||||
// only applies to DM's when the user was deactivated
|
||||
return {
|
||||
id: t('create_post.deactivated'),
|
||||
defaultMessage: 'You are viewing an archived channel with a deactivated user.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: t('archivedChannelMessage'),
|
||||
defaultMessage: 'You are viewing an **archived channel**. New messages cannot be posted.',
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.archivedWrapper}>
|
||||
<FormattedMarkdownText
|
||||
{...this.message()}
|
||||
theme={theme}
|
||||
style={style.archivedText}
|
||||
/>
|
||||
<Button
|
||||
containerStyle={style.closeButton}
|
||||
onPress={this.onCloseChannelPress}
|
||||
>
|
||||
<FormattedText
|
||||
id='center_panel.archived.closeChannel'
|
||||
defaultMessage='Close Channel'
|
||||
style={style.closeButtonText}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
archivedWrapper: {
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
borderTopWidth: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20),
|
||||
},
|
||||
archivedText: {
|
||||
textAlign: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
closeButton: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
alignItems: 'center',
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
borderRadius: 4,
|
||||
marginTop: 10,
|
||||
height: 40,
|
||||
},
|
||||
closeButtonText: {
|
||||
marginTop: 7,
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}));
|
||||
21
app/components/post_draft/archived/index.js
Normal file
21
app/components/post_draft/archived/index.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectPenultimateChannel} from '@actions/views/channel';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import Archived from './archived';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
teamId: getCurrentTeamId(state),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
selectPenultimateChannel,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(Archived);
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
|
@ -10,31 +9,35 @@ import {createPost} from '@mm-redux/actions/posts';
|
|||
import {setStatus} from '@mm-redux/actions/users';
|
||||
import {getCurrentChannel, isCurrentChannelReadOnly, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
|
||||
import {canUploadFilesOnMobile, getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getChannelTimezones} from '@mm-redux/actions/channels';
|
||||
|
||||
import {executeCommand} from 'app/actions/views/command';
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handlePostDraftChanged, selectPenultimateChannel} from 'app/actions/views/channel';
|
||||
import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, initUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app/actions/views/thread';
|
||||
import {userTyping} from 'app/actions/views/typing';
|
||||
import {handleClearFiles, handleClearFailedFiles, initUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
import {getAllowedServerMaxFileSize} from 'app/utils/file';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
import PostDraft from './post_draft';
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
export function mapStateToProps(state, ownProps) {
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
const config = getConfig(state);
|
||||
|
||||
const currentChannel = getCurrentChannel(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const status = getStatusForUserId(state, currentUserId);
|
||||
const userIsOutOfOffice = status === General.OUT_OF_OFFICE;
|
||||
const enableConfirmNotificationsToChannel = config?.EnableConfirmNotificationsToChannel === 'true';
|
||||
const currentChannelStats = getCurrentChannelStats(state);
|
||||
const membersCount = currentChannelStats?.member_count || 0; // eslint-disable-line camelcase
|
||||
const isTimezoneEnabled = config?.ExperimentalTimezone === 'true';
|
||||
|
||||
let deactivatedChannel = false;
|
||||
if (currentChannel && currentChannel.type === General.DM_CHANNEL) {
|
||||
const teammate = getChannelMembersForDm(state, currentChannel);
|
||||
|
|
@ -43,14 +46,6 @@ export function mapStateToProps(state, ownProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const status = getStatusForUserId(state, currentUserId);
|
||||
const userIsOutOfOffice = status === General.OUT_OF_OFFICE;
|
||||
const enableConfirmNotificationsToChannel = config?.EnableConfirmNotificationsToChannel === 'true';
|
||||
const currentChannelStats = getCurrentChannelStats(state);
|
||||
const currentChannelMembersCount = currentChannelStats?.member_count || 0; // eslint-disable-line camelcase
|
||||
const isTimezoneEnabled = config?.ExperimentalTimezone === 'true';
|
||||
|
||||
let canPost = true;
|
||||
let useChannelMentions = true;
|
||||
if (isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)) {
|
||||
|
|
@ -73,50 +68,36 @@ export function mapStateToProps(state, ownProps) {
|
|||
}
|
||||
|
||||
return {
|
||||
currentChannel,
|
||||
channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
|
||||
channelTeamId: currentChannel ? currentChannel.team_id : '',
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelDisplayName: state.views.channel.displayName || (currentChannel ? currentChannel.display_name : ''),
|
||||
channelIsReadOnly: isCurrentChannelReadOnly(state) || false,
|
||||
channelIsArchived: ownProps.channelIsArchived || (currentChannel ? currentChannel.delete_at !== 0 : false),
|
||||
currentUserId,
|
||||
userIsOutOfOffice,
|
||||
deactivatedChannel,
|
||||
files: currentDraft.files,
|
||||
maxFileSize: getAllowedServerMaxFileSize(config),
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH,
|
||||
theme: getTheme(state),
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
value: currentDraft.draft,
|
||||
enableConfirmNotificationsToChannel,
|
||||
currentChannelMembersCount,
|
||||
isTimezoneEnabled,
|
||||
isLandscape: isLandscape(state),
|
||||
canPost,
|
||||
channelDisplayName: state.views.channel.displayName || (currentChannel ? currentChannel.display_name : ''),
|
||||
channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
|
||||
channelIsArchived: ownProps.channelIsArchived || (currentChannel ? currentChannel.delete_at !== 0 : false),
|
||||
channelIsReadOnly: isCurrentChannelReadOnly(state) || false,
|
||||
currentUserId,
|
||||
deactivatedChannel,
|
||||
enableConfirmNotificationsToChannel,
|
||||
files: currentDraft.files,
|
||||
isLandscape: isLandscape(state),
|
||||
isTimezoneEnabled,
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH,
|
||||
maxFileSize: getAllowedServerMaxFileSize(config),
|
||||
membersCount,
|
||||
theme: getTheme(state),
|
||||
useChannelMentions,
|
||||
userIsOutOfOffice,
|
||||
value: currentDraft.draft,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addReactionToLatestPost,
|
||||
createPost,
|
||||
executeCommand,
|
||||
handleClearFiles,
|
||||
handleClearFailedFiles,
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
handleRemoveLastFile,
|
||||
initUploadFiles,
|
||||
userTyping,
|
||||
handleCommentDraftSelectionChanged,
|
||||
setStatus,
|
||||
selectPenultimateChannel,
|
||||
getChannelTimezones,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
addReactionToLatestPost,
|
||||
createPost,
|
||||
executeCommand,
|
||||
getChannelTimezones,
|
||||
handleClearFiles,
|
||||
handleClearFailedFiles,
|
||||
initUploadFiles,
|
||||
setStatus,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostTextbox);
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostDraft);
|
||||
|
|
@ -13,7 +13,7 @@ import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
|||
|
||||
import {mapStateToProps} from './index';
|
||||
|
||||
jest.mock('./post_textbox', () => ({
|
||||
jest.mock('./post_draft', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
624
app/components/post_draft/post_draft.js
Normal file
624
app/components/post_draft/post_draft.js
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert, Platform, ScrollView, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE, IS_REACTION_REGEX, MAX_FILE_COUNT} from '@constants/post_draft';
|
||||
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getFormattedFileSize} from '@mm-redux/utils/file_utils';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {confirmOutOfOfficeDisabled} from '@utils/status';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Archived from './archived';
|
||||
import PostInput from './post_input';
|
||||
import QuickActions from './quick_actions';
|
||||
import Typing from './typing';
|
||||
import Uploads from './uploads';
|
||||
|
||||
const AUTOCOMPLETE_MARGIN = 20;
|
||||
const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
export default class PostDraft extends PureComponent {
|
||||
static propTypes = {
|
||||
addReactionToLatestPost: PropTypes.func.isRequired,
|
||||
canPost: PropTypes.bool.isRequired,
|
||||
channelDisplayName: PropTypes.string,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsArchived: PropTypes.bool,
|
||||
channelIsReadOnly: PropTypes.bool.isRequired,
|
||||
createPost: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
cursorPositionEvent: PropTypes.string,
|
||||
deactivatedChannel: PropTypes.bool.isRequired,
|
||||
enableConfirmNotificationsToChannel: PropTypes.bool,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
files: PropTypes.array,
|
||||
getChannelTimezones: PropTypes.func.isRequired,
|
||||
handleClearFiles: PropTypes.func.isRequired,
|
||||
handleClearFailedFiles: PropTypes.func.isRequired,
|
||||
initUploadFiles: PropTypes.func.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
isTimezoneEnabled: PropTypes.bool,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
membersCount: PropTypes.number,
|
||||
rootId: PropTypes.string,
|
||||
screenId: PropTypes.string.isRequired,
|
||||
setStatus: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
useChannelMentions: PropTypes.bool.isRequired,
|
||||
userIsOutOfOffice: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
valueEvent: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
canPost: true,
|
||||
cursorPositionEvent: CHANNEL_POST_TEXTBOX_CURSOR_CHANGE,
|
||||
files: [],
|
||||
rootId: '',
|
||||
value: '',
|
||||
valueEvent: CHANNEL_POST_TEXTBOX_VALUE_CHANGE,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.input = React.createRef();
|
||||
|
||||
this.state = {
|
||||
top: 0,
|
||||
value: props.value,
|
||||
rootId: props.rootId,
|
||||
channelId: props.channelId,
|
||||
channelTimezoneCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(prevProps) {
|
||||
if (this.props.isTimezoneEnabled !== prevProps?.isTimezoneEnabled || prevProps?.channelId !== this.props.channelId) {
|
||||
this.numberOfTimezones().then((channelTimezoneCount) => this.setState({channelTimezoneCount}));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.input.current) {
|
||||
const {channelId, rootId, value} = this.props;
|
||||
const diffChannel = channelId !== prevProps.channelId;
|
||||
const diffThread = rootId !== prevProps.rootId;
|
||||
|
||||
if (diffChannel || diffThread) {
|
||||
const trimmed = value.trim();
|
||||
this.input.current.setValue(trimmed);
|
||||
this.updateInitialValue(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blurTextBox = () => {
|
||||
if (this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
}
|
||||
|
||||
canSend = () => {
|
||||
const {files, maxMessageLength} = this.props;
|
||||
const value = this.input.current?.getValue() || '';
|
||||
const messageLength = value.trim().length;
|
||||
|
||||
if (messageLength > maxMessageLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
const loadingComplete = !this.isFileLoading();
|
||||
return loadingComplete;
|
||||
}
|
||||
|
||||
return messageLength > 0;
|
||||
};
|
||||
|
||||
doSubmitMessage = () => {
|
||||
if (this.input.current) {
|
||||
const {createPost, currentUserId, channelId, files, handleClearFiles, rootId} = this.props;
|
||||
const value = this.input.current.getValue() || '';
|
||||
const postFiles = files.filter((f) => !f.failed);
|
||||
const post = {
|
||||
user_id: currentUserId,
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
message: value.trim(),
|
||||
};
|
||||
|
||||
createPost(post, postFiles);
|
||||
|
||||
if (postFiles.length) {
|
||||
handleClearFiles(channelId, rootId);
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, if the PostInput height increases from its
|
||||
// initial height (due to a multiline post or a post whose
|
||||
// message wraps, for example), then when the text is cleared
|
||||
// the PostInput height decrease will be animated. This
|
||||
// animation in conjunction with the PostList animation as it
|
||||
// receives the newly created post is causing issues in the iOS
|
||||
// PostList component as it fails to properly react to its content
|
||||
// size changes. While a proper fix is determined for the PostList
|
||||
// component, a small delay in triggering the height decrease
|
||||
// animation gives the PostList enough time to first handle content
|
||||
// size changes from the new post.
|
||||
setTimeout(() => {
|
||||
this.input.current.setValue('');
|
||||
this.setState({sendingMessage: false});
|
||||
}, 250);
|
||||
} else {
|
||||
this.input.current.setValue('');
|
||||
this.setState({sendingMessage: false});
|
||||
}
|
||||
|
||||
this.input.current.changeDraft('');
|
||||
|
||||
let callback;
|
||||
if (Platform.OS === 'android') {
|
||||
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
|
||||
// are typed successively without blurring the input
|
||||
const nextState = {
|
||||
keyboardType: 'email-address',
|
||||
};
|
||||
|
||||
callback = () => this.setState({keyboardType: 'default'});
|
||||
|
||||
this.setState(nextState, callback);
|
||||
}
|
||||
|
||||
EventEmitter.emit('scroll-to-bottom');
|
||||
}
|
||||
};
|
||||
|
||||
getStatusFromSlashCommand = (message) => {
|
||||
const tokens = message.split(' ');
|
||||
|
||||
if (tokens.length > 0) {
|
||||
return tokens[0].substring(1);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
handleInputQuickAction = (inputValue) => {
|
||||
if (this.input.current) {
|
||||
this.input.current.setValue(inputValue, true);
|
||||
this.input.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleLayout = (e) => {
|
||||
this.setState({
|
||||
top: e.nativeEvent.layout.y,
|
||||
});
|
||||
};
|
||||
|
||||
handlePasteFiles = (error, files) => {
|
||||
if (this.props.screenId === EphemeralStore.getNavigationTopComponentId()) {
|
||||
if (error) {
|
||||
this.showPasteFilesErrorDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const {maxFileSize} = this.props;
|
||||
const availableCount = MAX_FILE_COUNT - this.props.files.length;
|
||||
if (files.length > availableCount) {
|
||||
this.onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
const largeFile = files.find((image) => image.fileSize > maxFileSize);
|
||||
if (largeFile) {
|
||||
this.onShowFileSizeWarning(largeFile.fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleUploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleSendMessage = () => {
|
||||
if (!this.isSendButtonEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({sendingMessage: true});
|
||||
|
||||
const {channelId, files, handleClearFailedFiles, rootId} = this.props;
|
||||
const value = this.input.current?.getValue() || '';
|
||||
|
||||
const isReactionMatch = value.match(IS_REACTION_REGEX);
|
||||
if (isReactionMatch) {
|
||||
const emoji = isReactionMatch[2];
|
||||
this.sendReaction(emoji);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasFailedAttachments = files.some((f) => f.failed);
|
||||
if (hasFailedAttachments) {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.post_textbox.uploadFailedTitle',
|
||||
defaultMessage: 'Attachment failure',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.post_textbox.uploadFailedDesc',
|
||||
defaultMessage: 'Some attachments failed to upload to the server. Are you sure you want to post the message?',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
|
||||
onPress: () => {
|
||||
this.setState({sendingMessage: false});
|
||||
},
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: () => {
|
||||
// Remove only failed files
|
||||
handleClearFailedFiles(channelId, rootId);
|
||||
this.sendMessage();
|
||||
},
|
||||
}],
|
||||
);
|
||||
} else {
|
||||
this.sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
handleUploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (file.fileSize > this.props.maxFileSize) {
|
||||
this.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.initUploadFiles(files, this.props.rootId);
|
||||
}
|
||||
};
|
||||
|
||||
isFileLoading = () => {
|
||||
const {files} = this.props;
|
||||
|
||||
return files.some((file) => file.loading);
|
||||
};
|
||||
|
||||
isSendButtonEnabled = () => {
|
||||
return this.canSend() && !this.isFileLoading() && !this.state.sendingMessage;
|
||||
};
|
||||
|
||||
isStatusSlashCommand = (command) => {
|
||||
return command === General.ONLINE || command === General.AWAY ||
|
||||
command === General.DND || command === General.OFFLINE;
|
||||
};
|
||||
|
||||
onShowFileMaxWarning = () => {
|
||||
EventEmitter.emit('fileMaxWarning');
|
||||
};
|
||||
|
||||
onShowFileSizeWarning = (filename) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const fileSizeWarning = formatMessage({
|
||||
id: 'file_upload.fileAbove',
|
||||
defaultMessage: 'File above {max} cannot be uploaded: {filename}',
|
||||
}, {
|
||||
max: getFormattedFileSize({size: this.props.maxFileSize}),
|
||||
filename,
|
||||
});
|
||||
|
||||
EventEmitter.emit('fileSizeWarning', fileSizeWarning);
|
||||
setTimeout(() => {
|
||||
EventEmitter.emit('fileSizeWarning', null);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
numberOfTimezones = async () => {
|
||||
const {channelId, getChannelTimezones} = this.props;
|
||||
const {data} = await getChannelTimezones(channelId);
|
||||
return data?.length || 0;
|
||||
};
|
||||
|
||||
sendCommand = async (msg) => {
|
||||
const {intl} = this.context;
|
||||
const {channelId, executeCommand, rootId, userIsOutOfOffice} = this.props;
|
||||
|
||||
const status = this.getStatusFromSlashCommand(msg);
|
||||
if (userIsOutOfOffice && this.isStatusSlashCommand(status)) {
|
||||
confirmOutOfOfficeDisabled(intl, status, this.updateStatus);
|
||||
this.setState({sendingMessage: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {error} = await executeCommand(msg, channelId, rootId);
|
||||
this.setState({sendingMessage: false});
|
||||
|
||||
if (error) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.commands.error_title',
|
||||
defaultMessage: 'Error Executing Command',
|
||||
}),
|
||||
error.message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.input.current) {
|
||||
this.input.current.setValue('');
|
||||
this.input.current.changeDraft('');
|
||||
}
|
||||
};
|
||||
|
||||
sendMessage = () => {
|
||||
const value = this.input.current?.getValue() || '';
|
||||
const {enableConfirmNotificationsToChannel, membersCount, useChannelMentions} = this.props;
|
||||
const notificationsToChannel = enableConfirmNotificationsToChannel && useChannelMentions;
|
||||
const toAllOrChannel = this.textContainsAtAllAtChannel(value);
|
||||
|
||||
if (value.indexOf('/') === 0) {
|
||||
this.sendCommand(value);
|
||||
} else if (notificationsToChannel && membersCount > NOTIFY_ALL_MEMBERS && toAllOrChannel) {
|
||||
this.showSendToAllOrChannelAlert(membersCount);
|
||||
} else {
|
||||
this.doSubmitMessage();
|
||||
}
|
||||
};
|
||||
|
||||
sendReaction = (emoji) => {
|
||||
const {addReactionToLatestPost, rootId} = this.props;
|
||||
addReactionToLatestPost(emoji, rootId);
|
||||
|
||||
if (this.input.current) {
|
||||
this.input.current.setValue('');
|
||||
this.input.current.changeDraft('');
|
||||
}
|
||||
|
||||
this.setState({sendingMessage: false});
|
||||
};
|
||||
|
||||
showPasteFilesErrorDialog = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.files_paste.error_title',
|
||||
defaultMessage: 'Paste failed',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.files_paste.error_description',
|
||||
defaultMessage: 'An error occurred while pasting the file(s). Please try again.',
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.files_paste.error_dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
showSendToAllOrChannelAlert = (membersCount) => {
|
||||
const {intl} = this.context;
|
||||
const {channelTimezoneCount} = this.state;
|
||||
const {isTimezoneEnabled} = this.props;
|
||||
|
||||
let notifyAllMessage = '';
|
||||
if (isTimezoneEnabled && channelTimezoneCount) {
|
||||
notifyAllMessage = (
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.post_textbox.entire_channel.message.with_timezones',
|
||||
defaultMessage: 'By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}} in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?',
|
||||
},
|
||||
{
|
||||
totalMembers: membersCount - 1,
|
||||
timezones: channelTimezoneCount,
|
||||
},
|
||||
)
|
||||
);
|
||||
} else {
|
||||
notifyAllMessage = (
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.post_textbox.entire_channel.message',
|
||||
defaultMessage: 'By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Are you sure you want to do this?',
|
||||
},
|
||||
{
|
||||
totalMembers: membersCount - 1,
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.post_textbox.entire_channel.title',
|
||||
defaultMessage: 'Confirm sending notifications to entire channel',
|
||||
}),
|
||||
notifyAllMessage,
|
||||
[
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.post_textbox.entire_channel.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
onPress: () => {
|
||||
this.setState({sendingMessage: false});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.post_textbox.entire_channel.confirm',
|
||||
defaultMessage: 'Confirm',
|
||||
}),
|
||||
onPress: () => this.doSubmitMessage(),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
textContainsAtAllAtChannel = (text) => {
|
||||
const textWithoutCode = text.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g, '');
|
||||
return (/\B@(all|channel)\b/i).test(textWithoutCode);
|
||||
};
|
||||
|
||||
updateInitialValue = (value) => {
|
||||
this.setState({value});
|
||||
}
|
||||
|
||||
updateStatus = (status) => {
|
||||
const {currentUserId, setStatus} = this.props;
|
||||
setStatus({
|
||||
user_id: currentUserId,
|
||||
status,
|
||||
});
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const {channelIsArchived, deactivatedChannel, rootId, theme} = this.props;
|
||||
if (channelIsArchived || deactivatedChannel) {
|
||||
return (
|
||||
<Archived
|
||||
defactivated={deactivatedChannel}
|
||||
rootId={rootId}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
canPost,
|
||||
channelDisplayName,
|
||||
channelId,
|
||||
channelIsReadOnly,
|
||||
cursorPositionEvent,
|
||||
isLandscape,
|
||||
files,
|
||||
maxFileSize,
|
||||
maxMessageLength,
|
||||
screenId,
|
||||
valueEvent,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const readonly = channelIsReadOnly || !canPost;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typing theme={theme}/>
|
||||
{Platform.OS === 'android' &&
|
||||
<Autocomplete
|
||||
cursorPositionEvent={cursorPositionEvent}
|
||||
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
onChangeText={this.handleInputQuickAction}
|
||||
valueEvent={valueEvent}
|
||||
/>
|
||||
}
|
||||
<View
|
||||
style={[style.inputWrapper, padding(isLandscape)]}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
<ScrollView
|
||||
style={[style.inputContainer, readonly ? style.readonlyContainer : null]}
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
pinchGestureEnabled={false}
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
<PostInput
|
||||
channelDisplayName={channelDisplayName}
|
||||
channelId={channelId}
|
||||
cursorPositionEvent={cursorPositionEvent}
|
||||
inputEventType={valueEvent}
|
||||
isLandscape={isLandscape}
|
||||
maxMessageLength={maxMessageLength}
|
||||
onPasteFiles={this.handlePasteFiles}
|
||||
onSend={this.handleSendMessage}
|
||||
readonly={readonly}
|
||||
ref={this.input}
|
||||
rootId={rootId}
|
||||
screenId={screenId}
|
||||
theme={theme}
|
||||
updateInitialValue={this.updateInitialValue}
|
||||
/>
|
||||
<Uploads
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
theme={theme}
|
||||
/>
|
||||
<QuickActions
|
||||
blurTextBox={this.blurTextBox}
|
||||
canSend={this.isSendButtonEnabled()}
|
||||
fileCount={files.length}
|
||||
initialValue={this.state.value}
|
||||
inputEventType={valueEvent}
|
||||
maxFileSize={maxFileSize}
|
||||
onSend={this.handleSendMessage}
|
||||
onShowFileMaxWarning={this.onShowFileMaxWarning}
|
||||
onTextChange={this.handleInputQuickAction}
|
||||
onUploadFiles={this.handleUploadFiles}
|
||||
readonly={readonly}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
inputContentContainer: {
|
||||
alignItems: 'stretch',
|
||||
paddingTop: Platform.select({
|
||||
ios: 7,
|
||||
android: 0,
|
||||
}),
|
||||
},
|
||||
inputWrapper: {
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 2,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20),
|
||||
},
|
||||
readonlyContainer: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PostInput should match, full snapshot 1`] = `
|
||||
<ForwardRef(WrappedPasteableTextInput)
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
editable={true}
|
||||
keyboardAppearance="light"
|
||||
keyboardType="default"
|
||||
multiline={true}
|
||||
onChangeText={[Function]}
|
||||
onEndEditing={[Function]}
|
||||
onPaste={[MockFunction]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Write to Test Channel"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"maxHeight": 150,
|
||||
"minHeight": 30,
|
||||
"paddingBottom": 6,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingTop": 6,
|
||||
}
|
||||
}
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
`;
|
||||
18
app/components/post_draft/post_input/index.js
Normal file
18
app/components/post_draft/post_input/index.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {handlePostDraftChanged} from '@actions/views/channel';
|
||||
import {handleCommentDraftChanged} from '@actions/views/thread';
|
||||
import {userTyping} from '@actions/views/typing';
|
||||
|
||||
import PostInput from './post_input';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
userTyping,
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps, null, {forwardRef: true})(PostInput);
|
||||
323
app/components/post_draft/post_input/post_input.js
Normal file
323
app/components/post_draft/post_input/post_input.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert, AppState, findNodeHandle, Keyboard, NativeModules, Platform} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
|
||||
|
||||
import PasteableTextInput from '@components/pasteable_text_input';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {t} from '@utils/i18n';
|
||||
import {switchKeyboardForCodeBlocks} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
const {RNTextInputReset} = NativeModules;
|
||||
const INPUT_LINE_HEIGHT = 20;
|
||||
const HW_SHIFT_ENTER_TEXT = Platform.OS === 'ios' ? '\n' : '';
|
||||
|
||||
export default class PostInput extends PureComponent {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rootId: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
channelDisplayName: PropTypes.string,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
cursorPositionEvent: PropTypes.string,
|
||||
handleCommentDraftChanged: PropTypes.func.isRequired,
|
||||
handlePostDraftChanged: PropTypes.func.isRequired,
|
||||
inputEventType: PropTypes.string,
|
||||
isLandscape: PropTypes.bool,
|
||||
maxMessageLength: PropTypes.number,
|
||||
onPasteFiles: PropTypes.func.isRequired,
|
||||
onSend: PropTypes.func.isRequired,
|
||||
readonly: PropTypes.bool,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
updateInitialValue: PropTypes.func.isRequired,
|
||||
userTyping: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.input = React.createRef();
|
||||
this.cursorPosition = 0;
|
||||
this.value = '';
|
||||
|
||||
this.state = {
|
||||
keyboardType: 'default',
|
||||
longMessageAlertShown: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
|
||||
EventEmitter.on(event, this.handleInsertTextToDraft);
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
HWKeyboardEvent.onHWKeyPressed(this.handleHardwareEnterPress);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
|
||||
|
||||
EventEmitter.off(event, this.handleInsertTextToDraft);
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
HWKeyboardEvent.removeOnHWKeyPressed();
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
}
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
if (this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
changeDraft = (text) => {
|
||||
const {
|
||||
channelId,
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
rootId,
|
||||
} = this.props;
|
||||
|
||||
if (rootId) {
|
||||
handleCommentDraftChanged(rootId, text);
|
||||
} else {
|
||||
handlePostDraftChanged(channelId, text);
|
||||
}
|
||||
};
|
||||
|
||||
checkMessageLength = (value) => {
|
||||
const {intl} = this.context;
|
||||
const {maxMessageLength} = this.props;
|
||||
const valueLength = value.trim().length;
|
||||
|
||||
if (valueLength > maxMessageLength) {
|
||||
// Check if component is already aware message is too long
|
||||
if (!this.state.longMessageAlertShown) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.message_length.title',
|
||||
defaultMessage: 'Message Length',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.message_length.message',
|
||||
defaultMessage: 'Your current message is too long. Current character count: {count}/{max}',
|
||||
}, {
|
||||
max: maxMessageLength,
|
||||
count: valueLength,
|
||||
}),
|
||||
);
|
||||
this.setState({longMessageAlertShown: true});
|
||||
}
|
||||
} else if (this.state.longMessageAlertShown) {
|
||||
this.setState({longMessageAlertShown: false});
|
||||
}
|
||||
};
|
||||
|
||||
focus = () => {
|
||||
if (this.input.current) {
|
||||
this.input.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
getPlaceHolder = () => {
|
||||
const {readonly, rootId} = this.props;
|
||||
let placeholder;
|
||||
|
||||
if (readonly) {
|
||||
placeholder = {id: t('mobile.create_post.read_only'), defaultMessage: 'This channel is read-only.'};
|
||||
} else if (rootId) {
|
||||
placeholder = {id: t('create_comment.addComment'), defaultMessage: 'Add a comment...'};
|
||||
} else {
|
||||
placeholder = {id: t('create_post.write'), defaultMessage: 'Write to {channelDisplayName}'};
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
};
|
||||
|
||||
getValue = () => {
|
||||
return this.value;
|
||||
};
|
||||
|
||||
handleAndroidKeyboard = () => {
|
||||
this.blur();
|
||||
};
|
||||
|
||||
handleHardwareEnterPress = (keyEvent) => {
|
||||
switch (keyEvent.pressedKey) {
|
||||
case 'enter':
|
||||
this.props.onSend();
|
||||
break;
|
||||
case 'shift-enter':
|
||||
this.handleInsertTextToDraft(HW_SHIFT_ENTER_TEXT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleAppStateChange = (nextAppState) => {
|
||||
if (nextAppState !== 'active') {
|
||||
this.changeDraft(this.getValue());
|
||||
}
|
||||
};
|
||||
|
||||
handleEndEditing = (e) => {
|
||||
if (e && e.nativeEvent) {
|
||||
this.changeDraft(e.nativeEvent.text || '');
|
||||
}
|
||||
};
|
||||
|
||||
handlePostDraftSelectionChanged = (event, fromHandleTextChange = false) => {
|
||||
const cursorPosition = fromHandleTextChange ? this.state.cursorPosition : event.nativeEvent.selection.end;
|
||||
|
||||
const {cursorPositionEvent} = this.props;
|
||||
|
||||
if (cursorPositionEvent) {
|
||||
EventEmitter.emit(cursorPositionEvent, cursorPosition);
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
const keyboardType = switchKeyboardForCodeBlocks(this.state.value, cursorPosition);
|
||||
this.setState({cursorPosition, keyboardType});
|
||||
} else {
|
||||
this.setState({cursorPosition});
|
||||
}
|
||||
};
|
||||
|
||||
handleTextChange = (value, autocomplete = false) => {
|
||||
const {
|
||||
channelId,
|
||||
cursorPositionEvent,
|
||||
inputEventType,
|
||||
rootId,
|
||||
updateInitialValue,
|
||||
userTyping,
|
||||
} = this.props;
|
||||
this.value = value;
|
||||
updateInitialValue(value);
|
||||
|
||||
if (inputEventType) {
|
||||
EventEmitter.emit(inputEventType, value);
|
||||
}
|
||||
|
||||
const nextState = {value};
|
||||
|
||||
// Workaround for some Android keyboards that don't play well with cursors (e.g. Samsung keyboards)
|
||||
if (autocomplete && this.input?.current) {
|
||||
if (Platform.OS === 'android') {
|
||||
RNTextInputReset.resetKeyboardInput(findNodeHandle(this.input.current));
|
||||
} else {
|
||||
nextState.cursorPosition = value.length;
|
||||
if (cursorPositionEvent) {
|
||||
EventEmitter.emit(cursorPositionEvent, nextState.cursorPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkMessageLength(value);
|
||||
|
||||
// Workaround to avoid iOS emdash autocorrect in Code Blocks
|
||||
if (Platform.OS === 'ios') {
|
||||
const callback = () => this.handlePostDraftSelectionChanged(null, true);
|
||||
this.setState(nextState, callback);
|
||||
} else {
|
||||
this.setState(nextState);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
userTyping(channelId, rootId);
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertTextToDraft = (text) => {
|
||||
const {cursorPosition, value} = this.state;
|
||||
|
||||
let completed;
|
||||
if (value.length === 0) {
|
||||
completed = text;
|
||||
} else {
|
||||
const firstPart = value.substring(0, cursorPosition);
|
||||
const secondPart = value.substring(cursorPosition);
|
||||
completed = `${firstPart}${text}${secondPart}`;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: completed,
|
||||
});
|
||||
};
|
||||
|
||||
setValue = (value, autocomplete = false) => {
|
||||
this.value = value;
|
||||
if (this.input.current) {
|
||||
this.input.current.setNativeProps({
|
||||
text: value,
|
||||
});
|
||||
this.handleTextChange(value, autocomplete);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {channelDisplayName, isLandscape, onPasteFiles, readonly, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const placeholder = this.getPlaceHolder();
|
||||
let maxHeight = 150;
|
||||
|
||||
if (isLandscape) {
|
||||
maxHeight = 88;
|
||||
}
|
||||
|
||||
return (
|
||||
<PasteableTextInput
|
||||
ref={this.input}
|
||||
style={{...style.input, maxHeight}}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handlePostDraftSelectionChanged}
|
||||
placeholder={formatMessage(placeholder, {channelDisplayName})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
keyboardType={this.state.keyboardType}
|
||||
onEndEditing={this.handleEndEditing}
|
||||
disableFullscreenUI={true}
|
||||
editable={!readonly}
|
||||
onPaste={onPasteFiles}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
lineHeight: INPUT_LINE_HEIGHT,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: Platform.select({
|
||||
ios: 6,
|
||||
android: 8,
|
||||
}),
|
||||
paddingBottom: Platform.select({
|
||||
ios: 6,
|
||||
android: 2,
|
||||
}),
|
||||
minHeight: 30,
|
||||
},
|
||||
}));
|
||||
80
app/components/post_draft/post_input/post_input.test.js
Normal file
80
app/components/post_draft/post_input/post_input.test.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert} from 'react-native';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import PostInput from './post_input';
|
||||
|
||||
describe('PostInput', () => {
|
||||
const baseProps = {
|
||||
channelDisplayName: 'Test Channel',
|
||||
channelId: 'channel-id',
|
||||
cursorPositionEvent: '',
|
||||
handleCommentDraftChanged: jest.fn(),
|
||||
handlePostDraftChanged: jest.fn(),
|
||||
inputEventType: '',
|
||||
isLandscape: false,
|
||||
maxMessageLength: 4000,
|
||||
onPasteFiles: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
readonly: false,
|
||||
rootId: '',
|
||||
theme: Preferences.THEMES.default,
|
||||
updateInitialValue: jest.fn(),
|
||||
userTyping: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should emit the event but no text is save to draft', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.changeDraft = jest.fn();
|
||||
instance.handleAppStateChange('active');
|
||||
expect(instance.changeDraft).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should emit the event and text is save to draft', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
const value = 'some text';
|
||||
|
||||
instance.setValue(value);
|
||||
instance.handleAppStateChange('background');
|
||||
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value);
|
||||
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not send multiple alerts when message is too long', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
const longString = [...Array(baseProps.maxMessageLength + 2).keys()].map(() => Math.random().toString(36).slice(0, 1)).join('');
|
||||
|
||||
instance.handleTextChange(longString);
|
||||
instance.handleTextChange(longString.slice(1));
|
||||
|
||||
expect(Alert.alert).toBeCalled();
|
||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -3,7 +3,13 @@
|
|||
exports[`CameraButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -8,7 +8,7 @@ import Permissions from 'react-native-permissions';
|
|||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import CameraButton from './camera_button';
|
||||
import CameraQuickAction from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
|
|
@ -18,16 +18,16 @@ jest.mock('react-native-image-picker', () => ({
|
|||
describe('CameraButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
blurTextBox: jest.fn(),
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
onUploadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<CameraButton {...baseProps}/>);
|
||||
const wrapper = shallow(<CameraQuickAction {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -37,7 +37,7 @@ describe('CameraButton', () => {
|
|||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
<CameraQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe('CameraButton', () => {
|
|||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
<CameraQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ describe('CameraButton', () => {
|
|||
request.mockReturnValue(Permissions.RESULTS.GRANTED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
<CameraQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
|
@ -103,7 +103,7 @@ describe('CameraButton', () => {
|
|||
|
||||
test('should re-enable StatusBar after ImagePicker launchCamera finishes', async () => {
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
<CameraQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -7,69 +7,36 @@ import {
|
|||
Alert,
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
export default class CameraQuickAction extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileCount: PropTypes.number,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onUploadFiles: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
validMimeTypes: [],
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
maxFileCount: 5,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your camera',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_description',
|
||||
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
attachFileFromCamera = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
|
|
@ -99,11 +66,43 @@ export default class AttachmentButton extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.uploadFiles([response]);
|
||||
this.props.onUploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your camera',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_description',
|
||||
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
blurTextBox,
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
blurTextBox();
|
||||
this.attachFileFromCamera();
|
||||
};
|
||||
|
||||
hasCameraPermission = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const targetSource = Platform.OS === 'ios' ?
|
||||
|
|
@ -150,15 +149,20 @@ export default class AttachmentButton extends PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
const {disabled, theme} = this.props;
|
||||
const color = disabled ?
|
||||
changeOpacity(theme.centerChannelColor, 0.16) :
|
||||
changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.attachFileFromCamera}
|
||||
style={buttonContainerStyle}
|
||||
disabled={disabled}
|
||||
onPress={this.handleButtonPress}
|
||||
style={style.icon}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
color={color}
|
||||
name='camera-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
|
|
@ -166,3 +170,11 @@ export default class AttachmentButton extends PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileUploadButton should match snapshot 1`] = `
|
||||
exports[`FileQuickAction should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -8,24 +8,22 @@ import Permissions from 'react-native-permissions';
|
|||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import FileUploadButton from './file_upload_button';
|
||||
import FileQuickAction from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('FileUploadButton', () => {
|
||||
describe('FileQuickAction', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
blurTextBox: jest.fn(),
|
||||
browseFileTypes: '*',
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
onUploadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
|
@ -37,7 +35,7 @@ describe('FileUploadButton', () => {
|
|||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FileUploadButton {...baseProps}/>);
|
||||
const wrapper = shallow(<FileQuickAction {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -47,7 +45,7 @@ describe('FileUploadButton', () => {
|
|||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
<FileQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -63,7 +61,7 @@ describe('FileUploadButton', () => {
|
|||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
<FileQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -76,7 +74,7 @@ describe('FileUploadButton', () => {
|
|||
|
||||
test('hasStoragePermission returns true when permission has been granted', async () => {
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
<FileQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
|
@ -3,47 +3,33 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
NativeModules,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {Alert, NativeModules, Platform, StyleSheet} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export default class FileUploadButton extends PureComponent {
|
||||
export default class FileQuickAction extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileCount: PropTypes.number,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onUploadFiles: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
validMimeTypes: [],
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
extraOptions: null,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
|
@ -66,8 +52,8 @@ export default class FileUploadButton extends PureComponent {
|
|||
}
|
||||
|
||||
attachFileFromFiles = async () => {
|
||||
const {browseFileTypes} = this.props;
|
||||
const hasPermission = await this.hasStoragePermission();
|
||||
const browseFileTypes = Platform.OS === 'ios' ? 'public.item' : '*/*';
|
||||
|
||||
if (hasPermission) {
|
||||
try {
|
||||
|
|
@ -85,7 +71,7 @@ export default class FileUploadButton extends PureComponent {
|
|||
// Decode file uri to get the actual path
|
||||
res.uri = decodeURIComponent(res.uri);
|
||||
|
||||
this.props.uploadFiles([res]);
|
||||
this.props.onUploadFiles([res]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
@ -137,6 +123,7 @@ export default class FileUploadButton extends PureComponent {
|
|||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
blurTextBox,
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
|
|
@ -146,20 +133,26 @@ export default class FileUploadButton extends PureComponent {
|
|||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
this.props.blurTextBox();
|
||||
|
||||
blurTextBox();
|
||||
this.attachFileFromFiles();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
const {disabled, theme} = this.props;
|
||||
const color = disabled ?
|
||||
changeOpacity(theme.centerChannelColor, 0.16) :
|
||||
changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
disabled={disabled}
|
||||
onPress={this.handleButtonPress}
|
||||
style={buttonContainerStyle}
|
||||
style={style.icon}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
color={color}
|
||||
name='file-document-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
|
|
@ -167,3 +160,11 @@ export default class FileUploadButton extends PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImageUploadButton should match snapshot 1`] = `
|
||||
exports[`ImageQuickAction should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -8,14 +8,14 @@ import Permissions from 'react-native-permissions';
|
|||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import ImageUploadButton from './image_upload_button';
|
||||
import ImageQuickAction from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchImageLibrary: jest.fn().mockImplementation((options, callback) => callback({didCancel: true})),
|
||||
}));
|
||||
|
||||
describe('ImageUploadButton', () => {
|
||||
describe('ImageQuickAction', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
blurTextBox: jest.fn(),
|
||||
|
|
@ -23,12 +23,11 @@ describe('ImageUploadButton', () => {
|
|||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
onUploadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<ImageUploadButton {...baseProps}/>);
|
||||
const wrapper = shallow(<ImageQuickAction {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -38,7 +37,7 @@ describe('ImageUploadButton', () => {
|
|||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
<ImageQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ describe('ImageUploadButton', () => {
|
|||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
<ImageQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -83,7 +82,7 @@ describe('ImageUploadButton', () => {
|
|||
request.mockReturnValue(Permissions.RESULTS.GRANTED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
<ImageQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
|
@ -104,7 +103,7 @@ describe('ImageUploadButton', () => {
|
|||
|
||||
test('should re-enable StatusBar after ImagePicker launchImageLibrary finishes', async () => {
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
<ImageQuickAction {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
|
|
@ -3,77 +3,36 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import {Alert, Platform, StatusBar, StyleSheet} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
export default class ImageUploadButton extends PureComponent {
|
||||
export default class ImageQuickAction extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileCount: PropTypes.number,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onUploadFiles: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
validMimeTypes: [],
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
extraOptions: null,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
if (Platform.OS === 'android') {
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
attachFileFromLibrary = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
|
@ -101,11 +60,56 @@ export default class ImageUploadButton extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.uploadFiles([response]);
|
||||
this.props.onUploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
if (Platform.OS === 'android') {
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
blurTextBox,
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
blurTextBox();
|
||||
this.attachFileFromLibrary();
|
||||
};
|
||||
|
||||
hasPhotoPermission = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const targetSource = Platform.OS === 'ios' ?
|
||||
|
|
@ -151,32 +155,21 @@ export default class ImageUploadButton extends PureComponent {
|
|||
return true;
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.blurTextBox();
|
||||
this.attachFileFromLibrary();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
const {disabled, theme} = this.props;
|
||||
const color = disabled ?
|
||||
changeOpacity(theme.centerChannelColor, 0.16) :
|
||||
changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
disabled={disabled}
|
||||
onPress={this.handleButtonPress}
|
||||
style={buttonContainerStyle}
|
||||
style={style.icon}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
color={color}
|
||||
name='image-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
|
|
@ -184,3 +177,11 @@ export default class ImageUploadButton extends PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
16
app/components/post_draft/quick_actions/index.js
Normal file
16
app/components/post_draft/quick_actions/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {canUploadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
import QuickActions from './quick_actions';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(QuickActions);
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class InputQuickAction extends PureComponent {
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
inputType: PropTypes.oneOf(['at', 'slash']).isRequired,
|
||||
onTextChange: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: '',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
const {inputType, onTextChange, value} = this.props;
|
||||
|
||||
let newValue = '/';
|
||||
if (inputType === 'at') {
|
||||
newValue = `${value}@`;
|
||||
}
|
||||
|
||||
onTextChange(newValue);
|
||||
}
|
||||
|
||||
renderInput = (style) => {
|
||||
const {disabled, inputType, theme} = this.props;
|
||||
|
||||
if (inputType === 'at') {
|
||||
const color = disabled ?
|
||||
changeOpacity(theme.centerChannelColor, 0.16) :
|
||||
changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
color={color}
|
||||
name='at'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={require('assets/images/icons/slash-forward-box.png')}
|
||||
style={[style.slash, disabled ? style.disabled : null]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {disabled, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
disabled={disabled}
|
||||
onPress={this.onPress}
|
||||
style={style.icon}
|
||||
type={'opacity'}
|
||||
>
|
||||
{this.renderInput(style)}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
slash: {
|
||||
width: ICON_SIZE,
|
||||
height: ICON_SIZE,
|
||||
opacity: 1,
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
disabled: {
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
145
app/components/post_draft/quick_actions/quick_actions.js
Normal file
145
app/components/post_draft/quick_actions/quick_actions.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, StyleSheet, View} from 'react-native';
|
||||
|
||||
import {MAX_FILE_COUNT} from '@constants/post_draft';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import CameraAction from './camera_quick_action';
|
||||
import ImageAction from './image_quick_action';
|
||||
import FileAction from './file_quick_action';
|
||||
import InputAction from './input_quick_action';
|
||||
import SendAction from './send_action';
|
||||
|
||||
export default class QuickActions extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
canUploadFiles: PropTypes.bool,
|
||||
canSend: PropTypes.bool,
|
||||
fileCount: PropTypes.number,
|
||||
initialValue: PropTypes.string,
|
||||
inputEventType: PropTypes.string.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onSend: PropTypes.func.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func.isRequired,
|
||||
onTextChange: PropTypes.func.isRequired,
|
||||
onUploadFiles: PropTypes.func.isRequired,
|
||||
readonly: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
canUploadFiles: true,
|
||||
fileCount: 0,
|
||||
initialValue: '',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputValue: props.initialValue,
|
||||
atDisabled: props.readonly,
|
||||
slashDisabled: props.readonly,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on(this.props.inputEventType, this.handleInputEvent);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.readonly !== this.props.readonly || prevProps.initialValue !== this.props.initialValue) {
|
||||
this.handleInputEvent(this.props.initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off(this.props.inputEventType, this.handleInputEvent);
|
||||
}
|
||||
|
||||
handleInputEvent = (inputValue) => {
|
||||
const {readonly} = this.props;
|
||||
const atDisabled = readonly || inputValue[inputValue.length - 1] === '@';
|
||||
const slashDisabled = readonly || inputValue.length > 0;
|
||||
|
||||
this.setState({atDisabled, slashDisabled, inputValue});
|
||||
};
|
||||
|
||||
onShowFileMaxWarning = () => {
|
||||
EventEmitter.emit('fileMaxWarning');
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
blurTextBox,
|
||||
canUploadFiles,
|
||||
canSend,
|
||||
fileCount,
|
||||
onSend,
|
||||
onTextChange,
|
||||
onShowFileMaxWarning,
|
||||
readonly,
|
||||
theme,
|
||||
onUploadFiles,
|
||||
} = this.props;
|
||||
const uploadProps = {
|
||||
blurTextBox,
|
||||
disabled: !canUploadFiles || readonly,
|
||||
fileCount,
|
||||
maxFileCount: MAX_FILE_COUNT,
|
||||
onShowFileMaxWarning,
|
||||
theme,
|
||||
onUploadFiles,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.quickActionsContainer}>
|
||||
<InputAction
|
||||
disabled={this.state.atDisabled}
|
||||
inputType='at'
|
||||
onTextChange={onTextChange}
|
||||
theme={theme}
|
||||
value={this.state.inputValue}
|
||||
/>
|
||||
<InputAction
|
||||
disabled={this.state.slashDisabled}
|
||||
inputType='slash'
|
||||
onTextChange={onTextChange}
|
||||
theme={theme}
|
||||
/>
|
||||
<FileAction {...uploadProps}/>
|
||||
<ImageAction {...uploadProps}/>
|
||||
<CameraAction {...uploadProps}/>
|
||||
</View>
|
||||
<SendAction
|
||||
disabled={!canSend}
|
||||
handleSendMessage={onSend}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
quickActionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
exports[`SendAction should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -35,7 +35,7 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
|||
</View>
|
||||
`;
|
||||
|
||||
exports[`SendButton should match snapshot 1`] = `
|
||||
exports[`SendAction should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
|
|
@ -67,7 +67,7 @@ exports[`SendButton should match snapshot 1`] = `
|
|||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
|
||||
exports[`SendButton should render theme backgroundColor 1`] = `
|
||||
exports[`SendAction should render theme backgroundColor 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
|
|
@ -5,8 +5,8 @@ import React, {memo} from 'react';
|
|||
import {View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
let PaperPlane = null;
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ function SendButton(props) {
|
|||
const style = getStyleSheet(theme);
|
||||
|
||||
if (!PaperPlane) {
|
||||
PaperPlane = require('app/components/paper_plane').default;
|
||||
PaperPlane = require('./paper_plane').default;
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
|
|
@ -5,11 +5,11 @@ import React from 'react';
|
|||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import SendButton from 'app/components/send_button';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import SendAction from './index';
|
||||
|
||||
describe('SendButton', () => {
|
||||
describe('SendAction', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
handleSendMessage: jest.fn(),
|
||||
|
|
@ -18,7 +18,7 @@ describe('SendButton', () => {
|
|||
|
||||
function getWrapper(props = {}) {
|
||||
return shallow(
|
||||
<SendButton
|
||||
<SendAction
|
||||
{...baseProps}
|
||||
{...props}
|
||||
/>,
|
||||
|
|
@ -5,13 +5,10 @@ import {connect} from 'react-redux';
|
|||
|
||||
import {getUsersTyping} from '@mm-redux/selectors/entities/typing';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import Typing from './typing';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
typing: getUsersTyping(state),
|
||||
};
|
||||
}
|
||||
|
|
@ -2,28 +2,28 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {handleRemoveLastFile} from '@actions/views/file_upload';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getDimensions} from '@selectors/device';
|
||||
import {checkForFileUploadingInChannel} from '@selectors/file';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {checkForFileUploadingInChannel} from 'app/selectors/file';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
|
||||
import FileUploadPreview from './file_upload_preview';
|
||||
import Uploads from './uploads';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {deviceHeight} = getDimensions(state);
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
const channelId = getCurrentChannelId(state);
|
||||
|
||||
return {
|
||||
channelId,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
deviceHeight,
|
||||
files: currentDraft.files,
|
||||
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, channelId, ownProps.rootId),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FileUploadPreview);
|
||||
const mapDispatchToProps = {
|
||||
handleRemoveLastFile,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Uploads);
|
||||
17
app/components/post_draft/uploads/upload_item/index.js
Normal file
17
app/components/post_draft/uploads/upload_item/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload';
|
||||
|
||||
import UploadItem from './upload_item';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleRemoveFile,
|
||||
retryFileUpload,
|
||||
uploadComplete,
|
||||
uploadFailed,
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(UploadItem);
|
||||
|
|
@ -9,28 +9,26 @@ import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
|||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import FileAttachmentImage from '@components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from '@components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from '@components/file_upload_preview/file_upload_remove';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import ImageCacheManager from '@utils/image_cache_manager';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import UploadRemove from './upload_remove';
|
||||
import UploadRetry from './upload_retry';
|
||||
|
||||
export default class FileUploadItem extends PureComponent {
|
||||
export default class UploadItem extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
handleRemoveFile: PropTypes.func.isRequired,
|
||||
retryFileUpload: PropTypes.func.isRequired,
|
||||
uploadComplete: PropTypes.func.isRequired,
|
||||
uploadFailed: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
handleRemoveFile: PropTypes.func.isRequired,
|
||||
retryFileUpload: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadComplete: PropTypes.func.isRequired,
|
||||
uploadFailed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
|
@ -58,11 +56,11 @@ export default class FileUploadItem extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.actions.retryFileUpload(file, this.props.rootId);
|
||||
this.props.retryFileUpload(file, this.props.rootId);
|
||||
};
|
||||
|
||||
handleRemoveFile = (clientId, channelId, rootId) => {
|
||||
const {handleRemoveFile} = this.props.actions;
|
||||
const {handleRemoveFile} = this.props;
|
||||
if (this.uploadPromise) {
|
||||
this.uploadPromise.cancel(() => {
|
||||
this.canceled = true;
|
||||
|
|
@ -74,7 +72,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
};
|
||||
|
||||
handleUploadCompleted = (res) => {
|
||||
const {actions, channelId, file, rootId} = this.props;
|
||||
const {channelId, file, rootId, uploadComplete, uploadFailed} = this.props;
|
||||
const response = JSON.parse(res.data);
|
||||
if (res.respInfo.status === 200 || res.respInfo.status === 201) {
|
||||
const data = response.file_infos.map((f) => {
|
||||
|
|
@ -83,17 +81,17 @@ export default class FileUploadItem extends PureComponent {
|
|||
clientId: file.clientId,
|
||||
};
|
||||
});
|
||||
actions.uploadComplete(data, channelId, rootId);
|
||||
uploadComplete(data, channelId, rootId);
|
||||
} else {
|
||||
actions.uploadFailed([file.clientId], channelId, rootId, response.message);
|
||||
uploadFailed([file.clientId], channelId, rootId, response.message);
|
||||
}
|
||||
this.uploadPromise = null;
|
||||
};
|
||||
|
||||
handleUploadError = (error) => {
|
||||
const {actions, channelId, file, rootId} = this.props;
|
||||
const {channelId, file, rootId, uploadFailed} = this.props;
|
||||
if (!this.canceled) {
|
||||
actions.uploadFailed([file.clientId], channelId, rootId, error);
|
||||
uploadFailed([file.clientId], channelId, rootId, error);
|
||||
}
|
||||
this.uploadPromise = null;
|
||||
};
|
||||
|
|
@ -214,7 +212,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
<View style={styles.previewContainer}>
|
||||
{filePreviewComponent}
|
||||
{file.failed &&
|
||||
<FileUploadRetry
|
||||
<UploadRetry
|
||||
file={file}
|
||||
onPress={this.handleRetryFileUpload}
|
||||
/>
|
||||
|
|
@ -234,7 +232,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
</View>
|
||||
}
|
||||
</View>
|
||||
<FileUploadRemove
|
||||
<UploadRemove
|
||||
theme={this.props.theme}
|
||||
channelId={channelId}
|
||||
clientId={file.clientId}
|
||||
|
|
@ -4,17 +4,15 @@ import React from 'react';
|
|||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import FileUploadItem from './file_upload_item';
|
||||
import ImageCacheManager from '@utils/image_cache_manager';
|
||||
import UploadItem from './upload_item';
|
||||
|
||||
describe('FileUploadItem', () => {
|
||||
describe('UploadItem', () => {
|
||||
const props = {
|
||||
actions: {
|
||||
handleRemoveFile: jest.fn(),
|
||||
retryFileUpload: jest.fn(),
|
||||
uploadComplete: jest.fn(),
|
||||
uploadFailed: jest.fn(),
|
||||
},
|
||||
handleRemoveFile: jest.fn(),
|
||||
retryFileUpload: jest.fn(),
|
||||
uploadComplete: jest.fn(),
|
||||
uploadFailed: jest.fn(),
|
||||
channelId: 'channel-id',
|
||||
file: {
|
||||
loading: false,
|
||||
|
|
@ -24,7 +22,7 @@ describe('FileUploadItem', () => {
|
|||
|
||||
describe('downloadAndUploadFile', () => {
|
||||
test('should upload file', async () => {
|
||||
const component = shallow(<FileUploadItem {...props}/>);
|
||||
const component = shallow(<UploadItem {...props}/>);
|
||||
component.instance().uploadFile = jest.fn();
|
||||
await component.instance().downloadAndUploadFile({
|
||||
localPath: 'path/to/file',
|
||||
|
|
@ -36,7 +34,7 @@ describe('FileUploadItem', () => {
|
|||
|
||||
test('should download file if file path is http', async () => {
|
||||
jest.spyOn(ImageCacheManager, 'cache').mockReturnValue('path/to/downloaded/image');
|
||||
const component = shallow(<FileUploadItem {...props}/>);
|
||||
const component = shallow(<UploadItem {...props}/>);
|
||||
component.instance().uploadFile = jest.fn();
|
||||
await component.instance().downloadAndUploadFile({
|
||||
localPath: 'https://path.to/file',
|
||||
|
|
@ -47,7 +45,7 @@ describe('FileUploadItem', () => {
|
|||
});
|
||||
|
||||
test('should upload next file when we pass new props', async () => {
|
||||
const component = shallow(<FileUploadItem {...props}/>);
|
||||
const component = shallow(<UploadItem {...props}/>);
|
||||
component.instance().uploadFile = jest.fn();
|
||||
|
||||
component.setProps({file: {
|
||||
|
|
@ -5,11 +5,11 @@ import React, {PureComponent} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {View, Platform} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
export default class FileUploadRemove extends PureComponent {
|
||||
export default class UploadRemove extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string,
|
||||
clientId: PropTypes.string,
|
||||
|
|
@ -6,9 +6,9 @@ import PropTypes from 'prop-types';
|
|||
import {StyleSheet} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
|
||||
export default class FileUploadRetry extends PureComponent {
|
||||
export default class UploadRetry extends PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
InteractionManager,
|
||||
BackHandler,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
|
|
@ -14,21 +14,22 @@ import * as Animatable from 'react-native-animatable';
|
|||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import FileUploadItem from './file_upload_item';
|
||||
import UploadItem from './upload_item';
|
||||
|
||||
const showFiles = {opacity: 1, height: 68};
|
||||
const hideFiles = {opacity: 0, height: 0};
|
||||
const hideError = {height: 0};
|
||||
|
||||
export default class FileUploadPreview extends PureComponent {
|
||||
export default class Uploads extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
files: PropTypes.array.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
handleRemoveLastFile: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
@ -51,25 +52,33 @@ export default class FileUploadPreview extends PureComponent {
|
|||
EventEmitter.on('fileSizeWarning', this.handleFileSizeWarning);
|
||||
|
||||
if (this.props.files.length) {
|
||||
InteractionManager.runAfterInteractions(this.showOrHideContainer);
|
||||
this.showOrHideContainer();
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('fileMaxWarning', this.handleFileMaxWarning);
|
||||
EventEmitter.off('fileSizeWarning', this.handleFileSizeWarning);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.containerRef.current && this.props.files.length !== prevProps.files.length) {
|
||||
InteractionManager.runAfterInteractions(this.showOrHideContainer);
|
||||
this.showOrHideContainer();
|
||||
}
|
||||
}
|
||||
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
return (
|
||||
<FileUploadItem
|
||||
<UploadItem
|
||||
key={file.clientId}
|
||||
channelId={this.props.channelId}
|
||||
file={file}
|
||||
|
|
@ -89,6 +98,15 @@ export default class FileUploadPreview extends PureComponent {
|
|||
}, delay || 0);
|
||||
}
|
||||
|
||||
handleAndroidBack = () => {
|
||||
const {channelId, files, handleRemoveLastFile, rootId} = this.props;
|
||||
if (files.length) {
|
||||
handleRemoveLastFile(channelId, rootId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
handleFileMaxWarning = () => {
|
||||
this.setState({showFileMaxWarning: true});
|
||||
if (this.errorRef.current) {
|
||||
|
|
@ -1,340 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<Connect(Typing) />
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "flex-end",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderTopColor": "rgba(61,60,64,0.2)",
|
||||
"borderTopWidth": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"paddingBottom": 2,
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"paddingTop": 7,
|
||||
}
|
||||
}
|
||||
disableScrollViewPanResponder={true}
|
||||
keyboardShouldPersistTaps="always"
|
||||
overScrollMode="never"
|
||||
pinchGestureEnabled={false}
|
||||
scrollEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(WrappedPasteableTextInput)
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
editable={true}
|
||||
keyboardAppearance="light"
|
||||
keyboardType="default"
|
||||
multiline={true}
|
||||
onChangeText={[Function]}
|
||||
onContentSizeChange={null}
|
||||
onEndEditing={[Function]}
|
||||
onPaste={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Write to Test Channel"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"maxHeight": 150,
|
||||
"minHeight": 30,
|
||||
"paddingBottom": 6,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingTop": 6,
|
||||
}
|
||||
}
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<React.Fragment>
|
||||
<Connect(FileUploadPreview)
|
||||
files={Array []}
|
||||
rootId=""
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
"paddingBottom": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.64)"
|
||||
name="at"
|
||||
size={24}
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/icons/slash-forward-box.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"height": 24,
|
||||
"opacity": 1,
|
||||
"tintColor": "rgba(61,60,64,0.64)",
|
||||
"width": 24,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
<FileUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<ImageUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<AttachmentButton
|
||||
blurTextBox={[Function]}
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={true}
|
||||
handleSendMessage={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Svg, {G, Path} from 'react-native-svg';
|
||||
|
||||
export default class PaperClipIcon extends Component {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
setNativeProps = (nativeProps) => {
|
||||
if (this.root) {
|
||||
this.root.setNativeProps(nativeProps);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
x='0'
|
||||
y='0'
|
||||
viewBox='0 0 48 48'
|
||||
enableBackground='new 0 0 48 48'
|
||||
space='preserve'
|
||||
>
|
||||
<G>
|
||||
<Path
|
||||
d='M43.922,6.653c-2.643-2.644-6.201-4.107-9.959-4.069c-3.774,0.019-7.32,1.497-9.983,4.161l-12.3,12.3l-8.523,8.521 c-4.143,4.144-4.217,10.812-0.167,14.862c1.996,1.996,4.626,2.989,7.277,2.989c2.73,0,5.482-1.055,7.583-3.156l15.547-15.545 c0.002-0.002,0.002-0.004,0.004-0.005l5.358-5.358c1.394-1.393,2.176-3.24,2.201-5.2c0.026-1.975-0.716-3.818-2.09-5.192 c-2.834-2.835-7.496-2.787-10.394,0.108L9.689,29.857c-0.563,0.563-0.563,1.474,0,2.036c0.281,0.28,0.649,0.421,1.018,0.421 c0.369,0,0.737-0.141,1.018-0.421l18.787-18.788c1.773-1.774,4.609-1.824,6.322-0.11c0.82,0.82,1.263,1.928,1.247,3.119 c-0.017,1.205-0.497,2.342-1.357,3.201l-5.55,5.551c-0.002,0.002-0.002,0.004-0.004,0.005L15.814,40.225 c-3.02,3.02-7.86,3.094-10.789,0.167c-2.928-2.929-2.854-7.77,0.167-10.791l0.958-0.958c0.001-0.002,0.004-0.002,0.005-0.004 L26.016,8.78c2.123-2.124,4.951-3.303,7.961-3.317c2.998,0.02,5.814,1.13,7.91,3.226c4.35,4.351,4.309,11.472-0.093,15.873 L25.459,40.895c-0.563,0.562-0.563,1.473,0,2.035c0.281,0.281,0.65,0.422,1.018,0.422c0.369,0,0.737-0.141,1.018-0.422 L43.83,26.596C49.354,21.073,49.395,12.126,43.922,6.653z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
|
||||
import Typing from './components/typing';
|
||||
import PostTextBoxBase from './post_textbox_base';
|
||||
|
||||
const AUTOCOMPLETE_MARGIN = 20;
|
||||
const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
export default class PostTextBoxAndroid extends PostTextBoxBase {
|
||||
render() {
|
||||
const {
|
||||
deactivatedChannel,
|
||||
rootId,
|
||||
} = this.props;
|
||||
|
||||
if (deactivatedChannel) {
|
||||
return this.renderDeactivatedChannel();
|
||||
}
|
||||
|
||||
const {cursorPosition, top} = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typing/>
|
||||
<Autocomplete
|
||||
cursorPosition={cursorPosition}
|
||||
maxHeight={Math.min(top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
onChangeText={this.handleTextChange}
|
||||
value={this.state.value}
|
||||
rootId={rootId}
|
||||
/>
|
||||
{this.renderTextBox()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Typing from './components/typing';
|
||||
import PostTextBoxBase from './post_textbox_base';
|
||||
|
||||
export default class PostTextBoxIOS extends PostTextBoxBase {
|
||||
render() {
|
||||
const {deactivatedChannel} = this.props;
|
||||
|
||||
if (deactivatedChannel) {
|
||||
return this.renderDeactivatedChannel();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typing/>
|
||||
{this.renderTextBox()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,571 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, Image} from 'react-native';
|
||||
import assert from 'assert';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import SendButton from 'app/components/send_button';
|
||||
import PasteableTextInput from 'app/components/pasteable_text_input';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import FileUploadButton from './components/file_upload_button';
|
||||
import ImageUploadButton from './components/image_upload_button';
|
||||
import CameraButton from './components/camera_button';
|
||||
|
||||
import PostTextbox from './post_textbox.ios';
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('PostTextBox', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
addReactionToLatestPost: jest.fn(),
|
||||
createPost: jest.fn(),
|
||||
executeCommand: jest.fn(),
|
||||
handleCommentDraftChanged: jest.fn(),
|
||||
handlePostDraftChanged: jest.fn(),
|
||||
handleClearFiles: jest.fn(),
|
||||
handleClearFailedFiles: jest.fn(),
|
||||
handleRemoveLastFile: jest.fn(),
|
||||
initUploadFiles: jest.fn(),
|
||||
userTyping: jest.fn(),
|
||||
handleCommentDraftSelectionChanged: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
selectPenultimateChannel: jest.fn(),
|
||||
getChannelTimezones: jest.fn(),
|
||||
},
|
||||
canUploadFiles: true,
|
||||
channelId: 'channel-id',
|
||||
channelDisplayName: 'Test Channel',
|
||||
channelTeamId: 'channel-team-id',
|
||||
channelIsReadOnly: false,
|
||||
currentUserId: 'current-user-id',
|
||||
deactivatedChannel: false,
|
||||
files: [],
|
||||
maxFileSize: 1024,
|
||||
maxMessageLength: 4000,
|
||||
rootId: '',
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFileRequestStatus: 'NOT_STARTED',
|
||||
value: '',
|
||||
userIsOutOfOffice: false,
|
||||
channelIsArchived: false,
|
||||
onCloseChannel: jest.fn(),
|
||||
cursorPositionEvent: '',
|
||||
valueEvent: '',
|
||||
isLandscape: false,
|
||||
screenId: 'NavigationScreen1',
|
||||
canPost: true,
|
||||
currentChannelMembersCount: 50,
|
||||
enableConfirmNotificationsToChannel: true,
|
||||
useChannelMentions: true,
|
||||
};
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should emit the event but no text is save to draft', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.setState({value: 'some text'});
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.changeDraft = jest.fn();
|
||||
instance.handleAppStateChange('active');
|
||||
expect(instance.changeDraft).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should emit the event and text is save to draft', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
const value = 'some text';
|
||||
|
||||
wrapper.setState({value});
|
||||
instance.handleAppStateChange('background');
|
||||
expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value);
|
||||
expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not send multiple alerts when message is too long', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
const longString = [...Array(baseProps.maxMessageLength + 2).keys()].map(() => Math.random().toString(36).slice(0, 1)).join('');
|
||||
|
||||
instance.handleTextChange(longString);
|
||||
instance.handleTextChange(longString.slice(1));
|
||||
|
||||
expect(Alert.alert).toBeCalled();
|
||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should send an alert when sending a message with a channel mention', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
const message = '@all';
|
||||
const instance = wrapper.instance();
|
||||
instance.handleTextChange(message);
|
||||
|
||||
expect(wrapper.state('value')).toBe(message);
|
||||
|
||||
instance.sendMessage();
|
||||
expect(Alert.alert).toBeCalled();
|
||||
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
test('should not send an alert when sending a message with a channel mention when the user does not have channel mentions permission', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox
|
||||
{...baseProps}
|
||||
useChannelMentions={false}
|
||||
/>,
|
||||
);
|
||||
const message = '@all';
|
||||
const instance = wrapper.instance();
|
||||
instance.handleTextChange(message);
|
||||
|
||||
expect(wrapper.state('value')).toBe(message);
|
||||
|
||||
instance.sendMessage();
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return correct @all (same for @channel)', () => {
|
||||
for (const data of [
|
||||
{
|
||||
text: '',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: 'all',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@allison',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@ALLISON',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@all123',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '123@all',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: 'hey@all',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: 'hey@all.com',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@all',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@ALL',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@all hey',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'hey @all',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'HEY @ALL',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'hey @all!',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'hey @all:+1:',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'hey @ALL:+1:',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '`@all`',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@someone `@all`',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '``@all``',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '```@all```',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '```\n@all\n```',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '```````\n@all\n```````',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '```code\n@all\n```',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '~~~@all~~~',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '~~~\n@all\n~~~',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: ' /not_cmd @all',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@channel',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@channel.',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@channel/test',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: 'test/@channel',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@all/@channel',
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
text: '@cha*nnel*',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@cha**nnel**',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '*@cha*nnel',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '[@chan](https://google.com)nel',
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
text: '@channel',
|
||||
result: false,
|
||||
},
|
||||
]) {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
const containsAtChannel = wrapper.instance().textContainsAtAllAtChannel(data.text);
|
||||
assert.equal(containsAtChannel, data.result, data.text);
|
||||
}
|
||||
});
|
||||
|
||||
describe('send button', () => {
|
||||
test('should initially disable and hide the send button', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show a disabled send button when uploading a file', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [{loading: true}],
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show an enabled send button after uploading a file', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [{loading: false}],
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
test('should show an enabled send button with a message', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
value: 'test',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
test('should show a disabled send button while sending the message', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
value: 'test',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.setState({sendingMessage: true});
|
||||
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should preseve post message in the input field if the command failed', async () => {
|
||||
const props = {...baseProps};
|
||||
const errorResult = {error: {message: 'Error message'}};
|
||||
props.actions.executeCommand = jest.fn().
|
||||
mockResolvedValueOnce(errorResult).
|
||||
mockResolvedValue({data: 'success'});
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
const msg = '/fail preserve this text in the post draft';
|
||||
const instance = wrapper.instance();
|
||||
instance.handleTextChange(msg);
|
||||
expect(wrapper.state('value')).toBe(msg);
|
||||
|
||||
// On error, should prompt Alert dialog and should remain text value
|
||||
await instance.sendCommand(msg);
|
||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||
expect(Alert.alert).toHaveBeenCalledWith('Error Executing Command', errorResult.error.message);
|
||||
expect(wrapper.state('value')).toBe(msg);
|
||||
|
||||
// On success, should not prompt Alert dialog and should change text value to empty
|
||||
await instance.sendCommand(msg);
|
||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.state('value')).toBe('');
|
||||
});
|
||||
|
||||
describe('Paste images', () => {
|
||||
test('should show error dialog if error occured', () => {
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(null);
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
EphemeralStore.addNavigationComponentId('NavigationScreen1');
|
||||
wrapper.find(PasteableTextInput).first().simulate('paste', {error: 'some error'}, []);
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should show file max warning and not uploading', () => {
|
||||
jest.spyOn(EventEmitter, 'emit').mockReturnValue(null);
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
EphemeralStore.addNavigationComponentId('NavigationScreen1');
|
||||
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
]);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith('fileMaxWarning');
|
||||
expect(baseProps.actions.initUploadFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should show file size warning and not uploading', () => {
|
||||
jest.spyOn(EventEmitter, 'emit').mockReturnValue(null);
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox
|
||||
{...baseProps}
|
||||
maxFileSize={50 * 1024 * 1024}
|
||||
/>,
|
||||
);
|
||||
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
|
||||
{
|
||||
fileSize: 51 * 1024 * 1024,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
]);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith('fileSizeWarning', 'File above 50 MB cannot be uploaded: fileName.png');
|
||||
expect(baseProps.actions.initUploadFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should upload images', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
EphemeralStore.addNavigationComponentId('NavigationScreen1');
|
||||
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
]);
|
||||
expect(baseProps.actions.initUploadFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should NOT upload images when not the top most screen', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
EphemeralStore.addNavigationComponentId('NavigationScreen2');
|
||||
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
|
||||
{
|
||||
fileSize: 1000,
|
||||
fileName: 'fileName.png',
|
||||
type: 'images/png',
|
||||
url: 'path/to/image',
|
||||
},
|
||||
]);
|
||||
expect(baseProps.actions.initUploadFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should render all quick action icons', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
|
||||
// @ button
|
||||
expect(wrapper.find(MaterialCommunityIcons).exists()).toBe(true);
|
||||
|
||||
// slash command button
|
||||
expect(wrapper.find(Image).exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find(FileUploadButton).exists()).toBe(true);
|
||||
expect(wrapper.find(ImageUploadButton).exists()).toBe(true);
|
||||
expect(wrapper.find(CameraButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should trigger text change when @ icon is tapped', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.handleTextChange = jest.fn();
|
||||
|
||||
wrapper.find(MaterialCommunityIcons).parent().props().onPress();
|
||||
expect(instance.handleTextChange).toHaveBeenCalledWith(`${instance.state.value}@`, true);
|
||||
});
|
||||
|
||||
test('should disable slash icon if textbox value is NOT empty', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.setState({value: 'Test'});
|
||||
expect(wrapper.find(Image).parent().props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should NOT render file upload icons when server forbids it', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
canUploadFiles: false,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...props}/>);
|
||||
|
||||
expect(wrapper.find(MaterialCommunityIcons).exists()).toBe(true);
|
||||
expect(wrapper.find(Image).exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find(FileUploadButton).exists()).toBe(false);
|
||||
expect(wrapper.find(ImageUploadButton).exists()).toBe(false);
|
||||
expect(wrapper.find(CameraButton).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should change state value on props change', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
expect(wrapper.state('value')).toEqual('');
|
||||
wrapper.setProps({value: 'value', channelId: 'channel-id2'});
|
||||
expect(wrapper.state('value')).toEqual('value');
|
||||
});
|
||||
|
||||
test('should be disabled without the create_post permission', () => {
|
||||
const props = {...baseProps, canPost: false};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(PasteableTextInput).prop('editable')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -97,7 +97,7 @@ export default class MainSidebarIOS extends MainSidebarBase {
|
|||
};
|
||||
|
||||
open = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.openDrawer();
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default class SettingsDrawer extends SettingsSidebarBase {
|
|||
};
|
||||
|
||||
open = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
|
||||
if (this.drawerRef && !this.drawerOpened) {
|
||||
this.drawerRef.openDrawer();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const NavigationTypes = keyMirror({
|
|||
MAIN_SIDEBAR_DID_CLOSE: null,
|
||||
MAIN_SIDEBAR_DID_OPEN: null,
|
||||
CLOSE_SETTINGS_SIDEBAR: null,
|
||||
BLUR_POST_TEXTBOX: null,
|
||||
BLUR_POST_DRAFT: null,
|
||||
});
|
||||
|
||||
export default NavigationTypes;
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@ export const MAX_FILE_COUNT = 5;
|
|||
export const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
export const INSERT_TO_DRAFT = 'insert_to_draft';
|
||||
export const INSERT_TO_COMMENT = 'insert_to_comment';
|
||||
export const ICON_SIZE = 24;
|
||||
export const ICON_SIZE = 24;
|
||||
export const ACCESSORIES_CONTAINER_NATIVE_ID = 'channelAccessoriesContainer';
|
||||
export const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
|
||||
export const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
|
||||
|
|
@ -108,10 +108,10 @@ Navigation.events().registerAppLaunchedListener(() => {
|
|||
switch (componentId) {
|
||||
case 'MainSidebar':
|
||||
EventEmitter.emit(NavigationTypes.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
EventEmitter.emit(Navigation.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(Navigation.BLUR_POST_DRAFT);
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@
|
|||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {openMainSideMenu, openSettingsSideMenu} from '@actions/navigation';
|
||||
import KeyboardLayout from '@components/layout/keyboard_layout';
|
||||
import InteractiveDialogController from '@components/interactive_dialog_controller';
|
||||
import NetworkIndicator from '@components/network_indicator';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {openMainSideMenu, openSettingsSideMenu} from 'app/actions/navigation';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
|
||||
import NetworkIndicator from 'app/components/network_indicator';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
|
||||
import LocalConfig from 'assets/config';
|
||||
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
|
|
@ -22,12 +21,12 @@ import ChannelBase, {ClientUpgradeListener} from './channel_base';
|
|||
|
||||
export default class ChannelAndroid extends ChannelBase {
|
||||
openMainSidebar = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
openMainSideMenu();
|
||||
};
|
||||
|
||||
openSettingsSidebar = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
openSettingsSideMenu();
|
||||
};
|
||||
|
||||
|
|
@ -49,8 +48,8 @@ export default class ChannelAndroid extends ChannelBase {
|
|||
<View style={style.flex}>
|
||||
<ChannelPostList/>
|
||||
</View>
|
||||
<PostTextbox
|
||||
ref={this.postTextbox}
|
||||
<PostDraft
|
||||
ref={this.postDraft}
|
||||
screenId={this.props.componentId}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@ import React from 'react';
|
|||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
|
||||
import MainSidebar from 'app/components/sidebars/main';
|
||||
import NetworkIndicator from 'app/components/network_indicator';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import SettingsSidebar from 'app/components/sidebars/settings';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from '@components/autocomplete';
|
||||
import InteractiveDialogController from '@components/interactive_dialog_controller';
|
||||
import NetworkIndicator from '@components/network_indicator';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import SafeAreaView from '@components/safe_area_view';
|
||||
import MainSidebar from '@components/sidebars/main';
|
||||
import SettingsSidebar from '@components/sidebars/settings';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {ACCESSORIES_CONTAINER_NATIVE_ID, CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE} from '@constants/post_draft';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import LocalConfig from 'assets/config';
|
||||
|
||||
|
|
@ -21,11 +22,13 @@ import ChannelBase, {ClientUpgradeListener} from './channel_base';
|
|||
import ChannelNavBar from './channel_nav_bar';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
const ACCESSORIES_CONTAINER_NATIVE_ID = 'channelAccessoriesContainer';
|
||||
const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
|
||||
const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
|
||||
|
||||
export default class ChannelIOS extends ChannelBase {
|
||||
handleAutoComplete = (value) => {
|
||||
if (this.postDraft?.current) {
|
||||
this.postDraft.current.handleInputQuickAction(value);
|
||||
}
|
||||
};
|
||||
|
||||
mainSidebarRef = (ref) => {
|
||||
if (ref) {
|
||||
this.mainSidebar = ref;
|
||||
|
|
@ -87,10 +90,10 @@ export default class ChannelIOS extends ChannelBase {
|
|||
scrollViewNativeID={currentChannelId}
|
||||
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
|
||||
>
|
||||
<PostTextbox
|
||||
<PostDraft
|
||||
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
|
||||
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
|
||||
ref={this.postTextbox}
|
||||
ref={this.postDraft}
|
||||
screenId={this.props.componentId}
|
||||
/>
|
||||
</KeyboardTrackingView>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default class ChannelBase extends PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.postTextbox = React.createRef();
|
||||
this.postDraft = React.createRef();
|
||||
this.keyboardTracker = React.createRef();
|
||||
|
||||
this.state = {
|
||||
|
|
@ -71,7 +71,7 @@ export default class ChannelBase extends PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on(NavigationTypes.BLUR_POST_TEXTBOX, this.blurPostTextBox);
|
||||
EventEmitter.on(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
|
||||
EventEmitter.on('leave_team', this.handleLeaveTeam);
|
||||
|
||||
if (this.props.currentTeamId) {
|
||||
|
|
@ -134,13 +134,13 @@ export default class ChannelBase extends PureComponent {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off(NavigationTypes.BLUR_POST_TEXTBOX, this.blurPostTextBox);
|
||||
EventEmitter.off(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
|
||||
EventEmitter.off('leave_team', this.handleLeaveTeam);
|
||||
}
|
||||
|
||||
blurPostTextBox = () => {
|
||||
if (this.postTextbox?.current) {
|
||||
this.postTextbox.current.blur();
|
||||
blurPostDraft = () => {
|
||||
if (this.postDraft?.current) {
|
||||
this.postDraft.current.blurTextBox();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -190,12 +190,6 @@ export default class ChannelBase extends PureComponent {
|
|||
});
|
||||
}, 1000);
|
||||
|
||||
handleAutoComplete = (value) => {
|
||||
if (this.postTextbox?.current) {
|
||||
this.postTextbox.current.handleTextChange(value, true);
|
||||
}
|
||||
};
|
||||
|
||||
handleLeaveTeam = () => {
|
||||
this.props.actions.selectDefaultTeam();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,11 +56,10 @@ exports[`thread should match snapshot, has root post 1`] = `
|
|||
accessoriesContainerID="threadAccessoriesContainer"
|
||||
scrollViewNativeID="threadPostList"
|
||||
>
|
||||
<Connect(PostTextBoxIOS)
|
||||
<Connect(PostDraft)
|
||||
channelId="channel_id"
|
||||
channelIsArchived={false}
|
||||
cursorPositionEvent="onThreadTextBoxCursorChange"
|
||||
onCloseChannel={[Function]}
|
||||
rootId="root_id"
|
||||
valueEvent="onThreadTextBoxValueChange"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {THREAD} from 'app/constants/screen';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import KeyboardLayout from '@components/layout/keyboard_layout';
|
||||
import Loading from '@components/loading';
|
||||
import PostList from '@components/post_list';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import SafeAreaView from '@components/safe_area_view';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ThreadBase from './thread_base';
|
||||
|
||||
|
|
@ -27,29 +27,27 @@ export default class ThreadAndroid extends ThreadBase {
|
|||
} = this.props;
|
||||
|
||||
let content;
|
||||
let postTextBox;
|
||||
if (this.hasRootPost()) {
|
||||
content = (
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
lastPostIndex={-1}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
/>
|
||||
);
|
||||
|
||||
postTextBox = (
|
||||
<PostTextbox
|
||||
channelId={channelId}
|
||||
channelIsArchived={channelIsArchived}
|
||||
onCloseChannel={this.onCloseChannel}
|
||||
rootId={rootId}
|
||||
screenId={this.props.componentId}
|
||||
/>
|
||||
<>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
lastPostIndex={-1}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
/>
|
||||
<PostDraft
|
||||
ref={this.postDraft}
|
||||
channelId={channelId}
|
||||
channelIsArchived={channelIsArchived}
|
||||
rootId={rootId}
|
||||
screenId={this.props.componentId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
|
|
@ -64,7 +62,6 @@ export default class ThreadAndroid extends ThreadBase {
|
|||
<KeyboardLayout>
|
||||
<View style={style.separator}/>
|
||||
{content}
|
||||
{postTextBox}
|
||||
</KeyboardLayout>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ import React from 'react';
|
|||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from '@components/autocomplete';
|
||||
import Loading from '@components/loading';
|
||||
import PostList from '@components/post_list';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import SafeAreaView from '@components/safe_area_view';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {getLastPostIndex} from '@mm-redux/utils/post_list';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {THREAD} from 'app/constants/screen';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ThreadBase from './thread_base';
|
||||
|
||||
|
|
@ -24,6 +23,12 @@ const THREAD_POST_TEXTBOX_VALUE_CHANGE = 'onThreadTextBoxValueChange';
|
|||
const SCROLLVIEW_NATIVE_ID = 'threadPostList';
|
||||
|
||||
export default class ThreadIOS extends ThreadBase {
|
||||
handleAutoComplete = (value) => {
|
||||
if (this.postDraft?.current) {
|
||||
this.postDraft.current.handleInputQuickAction(value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
|
|
@ -35,10 +40,10 @@ export default class ThreadIOS extends ThreadBase {
|
|||
} = this.props;
|
||||
|
||||
let content;
|
||||
let postTextBox;
|
||||
let postDraft;
|
||||
if (this.hasRootPost()) {
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
|
|
@ -59,20 +64,19 @@ export default class ThreadIOS extends ThreadBase {
|
|||
rootId={rootId}
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
|
||||
postTextBox = (
|
||||
postDraft = (
|
||||
<KeyboardTrackingView
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
|
||||
>
|
||||
<PostTextbox
|
||||
<PostDraft
|
||||
channelId={channelId}
|
||||
channelIsArchived={channelIsArchived}
|
||||
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
|
||||
onCloseChannel={this.onCloseChannel}
|
||||
ref={this.postTextbox}
|
||||
ref={this.postDraft}
|
||||
rootId={rootId}
|
||||
screenId={this.props.componentId}
|
||||
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
|
||||
|
|
@ -96,7 +100,7 @@ export default class ThreadIOS extends ThreadBase {
|
|||
<StatusBar/>
|
||||
{content}
|
||||
</SafeAreaView>
|
||||
{postTextBox}
|
||||
{postDraft}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import Preferences from '@mm-redux/constants/preferences';
|
|||
import {General, RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import PostList from 'app/components/post_list';
|
||||
import * as NavigationActions from 'app/actions/navigation';
|
||||
|
||||
import ThreadIOS from './thread.ios';
|
||||
|
||||
|
|
@ -53,23 +52,6 @@ describe('thread', () => {
|
|||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call resetToChannel on onCloseChannel', () => {
|
||||
const resetToChannel = jest.spyOn(NavigationActions, 'resetToChannel');
|
||||
|
||||
const passProps = {
|
||||
disableTermsModal: true,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ThreadIOS
|
||||
{...baseProps}
|
||||
/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
wrapper.instance().onCloseChannel();
|
||||
expect(resetToChannel).toHaveBeenCalledTimes(1);
|
||||
expect(resetToChannel).toBeCalledWith(passProps);
|
||||
});
|
||||
|
||||
test('should match snapshot, render footer', () => {
|
||||
const wrapper = shallow(
|
||||
<ThreadIOS {...baseProps}/>,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {General, RequestStatus} from '@mm-redux/constants';
|
|||
|
||||
import Loading from 'app/components/loading';
|
||||
import DeletedPost from 'app/components/deleted_post';
|
||||
import {resetToChannel, popTopScreen, mergeNavigationOptions} from 'app/actions/navigation';
|
||||
import {popTopScreen, mergeNavigationOptions} from 'app/actions/navigation';
|
||||
|
||||
export default class ThreadBase extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -50,7 +50,7 @@ export default class ThreadBase extends PureComponent {
|
|||
title = formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: displayName});
|
||||
}
|
||||
|
||||
this.postTextbox = React.createRef();
|
||||
this.postDraft = React.createRef();
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
|
|
@ -86,12 +86,6 @@ export default class ThreadBase extends PureComponent {
|
|||
popTopScreen(componentId);
|
||||
};
|
||||
|
||||
handleAutoComplete = (value) => {
|
||||
if (this.postTextbox?.current) {
|
||||
this.postTextbox.current.handleTextChange(value, true);
|
||||
}
|
||||
};
|
||||
|
||||
hasRootPost = () => {
|
||||
return this.props.postIds.includes(this.props.rootId);
|
||||
};
|
||||
|
|
@ -115,11 +109,4 @@ export default class ThreadBase extends PureComponent {
|
|||
|
||||
return null;
|
||||
};
|
||||
|
||||
onCloseChannel = () => {
|
||||
const passProps = {
|
||||
disableTermsModal: true,
|
||||
};
|
||||
resetToChannel(passProps);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import {Preferences} from '@mm-redux/constants';
|
|||
import {getFormattedFileSize, lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import PaperPlane from 'app/components/paper_plane';
|
||||
import {MAX_FILE_COUNT} from 'app/constants/post_textbox';
|
||||
import PaperPlane from 'app/components/post_draft/quick_actions/send_action/paper_plane';
|
||||
import {MAX_FILE_COUNT} from 'app/constants/post_draft';
|
||||
import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {getExtensionFromMime} from 'app/utils/file';
|
||||
|
|
|
|||
Loading…
Reference in a new issue