forked from Ivasoft/mattermost-mobile
669 lines
21 KiB
JavaScript
669 lines
21 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {PureComponent} from 'react';
|
|
import {NavigationActions} from 'react-navigation';
|
|
import TouchableItem from 'react-navigation-stack/dist/views/TouchableItem';
|
|
import PropTypes from 'prop-types';
|
|
import {intlShape} from 'react-intl';
|
|
|
|
import {
|
|
Image,
|
|
NativeModules,
|
|
PermissionsAndroid,
|
|
ScrollView,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
|
import Video from 'react-native-video';
|
|
import LocalAuth from 'react-native-local-auth';
|
|
import RNFetchBlob from 'rn-fetch-blob';
|
|
|
|
import {Preferences} from 'mattermost-redux/constants';
|
|
import {getFormattedFileSize, lookupMimeType} from 'mattermost-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 mattermostManaged from 'app/mattermost_managed';
|
|
import {getExtensionFromMime} from 'app/utils/file';
|
|
import {emptyFunction} from 'app/utils/general';
|
|
import {preventDoubleTap} from 'app/utils/tap';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
|
|
|
import {
|
|
ExcelSvg,
|
|
GenericSvg,
|
|
PdfSvg,
|
|
PptSvg,
|
|
ZipSvg,
|
|
} from 'share_extension/common/icons';
|
|
|
|
import ChannelButton from './channel_button';
|
|
import TeamButton from './team_button';
|
|
|
|
const defaultTheme = Preferences.THEMES.default;
|
|
const extensionSvg = {
|
|
csv: ExcelSvg,
|
|
pdf: PdfSvg,
|
|
ppt: PptSvg,
|
|
pptx: PptSvg,
|
|
xls: ExcelSvg,
|
|
xlsx: ExcelSvg,
|
|
zip: ZipSvg,
|
|
};
|
|
const ShareExtension = NativeModules.MattermostShare;
|
|
const INPUT_HEIGHT = 150;
|
|
const MAX_MESSAGE_LENGTH = 4000;
|
|
|
|
export default class ExtensionPost extends PureComponent {
|
|
static propTypes = {
|
|
actions: PropTypes.shape({
|
|
getTeamChannels: PropTypes.func.isRequired,
|
|
}).isRequired,
|
|
channelId: PropTypes.string,
|
|
channels: PropTypes.object.isRequired,
|
|
currentUserId: PropTypes.string.isRequired,
|
|
maxFileSize: PropTypes.number.isRequired,
|
|
navigation: PropTypes.object.isRequired,
|
|
teamId: PropTypes.string.isRequired,
|
|
token: PropTypes.string,
|
|
url: PropTypes.string,
|
|
};
|
|
|
|
static contextTypes = {
|
|
intl: intlShape,
|
|
};
|
|
|
|
static navigationOptions = ({navigation}) => {
|
|
const {params = {}} = navigation.state;
|
|
const title = params.title || '';
|
|
const headerLeft = (
|
|
<TouchableItem
|
|
accessibilityComponentType='button'
|
|
accessibilityTraits='button'
|
|
borderless={true}
|
|
delayPressIn={0}
|
|
pressColorAndroid='rgba(0, 0, 0, .32)'
|
|
onPress={params.close ? params.close : emptyFunction}
|
|
>
|
|
<View style={styles.left}>
|
|
<MaterialIcon
|
|
name='close'
|
|
style={styles.closeButton}
|
|
/>
|
|
</View>
|
|
</TouchableItem>
|
|
);
|
|
|
|
let headerRight = null;
|
|
|
|
if (params.post) {
|
|
headerRight = (
|
|
<TouchableItem
|
|
accessibilityComponentType='button'
|
|
accessibilityTraits='button'
|
|
borderless={true}
|
|
delayPressIn={0}
|
|
pressColorAndroid='rgba(0, 0, 0, .32)'
|
|
onPress={params.post}
|
|
>
|
|
<View style={styles.left}>
|
|
<PaperPlane
|
|
color={defaultTheme.sidebarHeaderTextColor}
|
|
height={20}
|
|
width={20}
|
|
/>
|
|
</View>
|
|
</TouchableItem>
|
|
);
|
|
}
|
|
|
|
return {headerLeft, headerRight, title};
|
|
};
|
|
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
|
|
props.navigation.setParams({
|
|
title: context.intl.formatMessage({
|
|
id: 'mobile.extension.title',
|
|
defaultMessage: 'Share in Mattermost',
|
|
}),
|
|
});
|
|
|
|
this.state = {
|
|
channelId: props.channelId,
|
|
files: [],
|
|
hasPermission: null,
|
|
teamId: props.teamId,
|
|
totalSize: 0,
|
|
value: '',
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.props.navigation.setParams({
|
|
close: this.onClose,
|
|
});
|
|
this.auth();
|
|
}
|
|
|
|
auth = async () => {
|
|
try {
|
|
const {formatMessage} = this.context.intl;
|
|
const config = await mattermostManaged.getConfig();
|
|
|
|
if (config) {
|
|
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
|
|
const vendor = config.vendor || 'Mattermost';
|
|
|
|
if (authNeeded) {
|
|
const isSecured = await mattermostManaged.isDeviceSecure();
|
|
if (isSecured) {
|
|
try {
|
|
await LocalAuth.auth({
|
|
reason: formatMessage({
|
|
id: 'mobile.managed.secured_by',
|
|
defaultMessage: 'Secured by {vendor}',
|
|
}, {vendor}),
|
|
fallbackToPasscode: true,
|
|
suppressEnterPassword: false,
|
|
});
|
|
} catch (err) {
|
|
return this.onClose({nativeEvent: true});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// do nothing
|
|
}
|
|
|
|
return this.initialize();
|
|
};
|
|
|
|
getInputRef = (ref) => {
|
|
this.input = ref;
|
|
};
|
|
|
|
goToChannels = preventDoubleTap(() => {
|
|
const {formatMessage} = this.context.intl;
|
|
const {navigation} = this.props;
|
|
const navigateAction = NavigationActions.navigate({
|
|
routeName: 'Channels',
|
|
params: {
|
|
title: formatMessage({
|
|
id: 'mobile.routes.selectChannel',
|
|
defaultMessage: 'Select Channel',
|
|
}),
|
|
currentChannelId: this.state.channelId,
|
|
onSelectChannel: this.handleSelectChannel,
|
|
},
|
|
});
|
|
navigation.dispatch(navigateAction);
|
|
});
|
|
|
|
goToTeams = preventDoubleTap(() => {
|
|
const {formatMessage} = this.context.intl;
|
|
const {navigation} = this.props;
|
|
const navigateAction = NavigationActions.navigate({
|
|
routeName: 'Teams',
|
|
params: {
|
|
title: formatMessage({
|
|
id: 'mobile.routes.selectTeam',
|
|
defaultMessage: 'Select Team',
|
|
}),
|
|
currentTeamId: this.state.teamId,
|
|
onSelectTeam: this.handleSelectTeam,
|
|
},
|
|
});
|
|
navigation.dispatch(navigateAction);
|
|
});
|
|
|
|
handleBlur = () => {
|
|
if (this.input) {
|
|
this.input.setNativeProps({
|
|
autoScroll: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
handleFocus = () => {
|
|
if (this.input) {
|
|
this.input.setNativeProps({
|
|
autoScroll: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
handleSelectChannel = (channelId) => {
|
|
this.setState({channelId});
|
|
};
|
|
|
|
handleSelectTeam = (teamId, defaultChannelId) => {
|
|
this.setState({teamId, channelId: defaultChannelId});
|
|
};
|
|
|
|
handleTextChange = (value) => {
|
|
this.setState({value});
|
|
};
|
|
|
|
initialize = async () => {
|
|
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
|
|
let granted;
|
|
if (!hasPermission) {
|
|
granted = await PermissionsAndroid.request(
|
|
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE
|
|
);
|
|
}
|
|
|
|
if (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED) {
|
|
const data = await ShareExtension.data();
|
|
return this.loadData(data);
|
|
}
|
|
|
|
return this.setState({hasPermission: false});
|
|
};
|
|
|
|
loadData = async (items) => {
|
|
const {actions, maxFileSize, teamId, token, url} = this.props;
|
|
if (token && url) {
|
|
const text = [];
|
|
const files = [];
|
|
let totalSize = 0;
|
|
let error;
|
|
|
|
actions.getTeamChannels(teamId);
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i];
|
|
switch (item.type) {
|
|
case 'text/plain':
|
|
text.push(item.value);
|
|
break;
|
|
default: {
|
|
let fileSize = {size: 0};
|
|
const fullPath = item.value;
|
|
try {
|
|
fileSize = await RNFetchBlob.fs.stat(fullPath); // eslint-disable-line no-await-in-loop
|
|
} catch (e) {
|
|
const {formatMessage} = this.context.intl;
|
|
error = formatMessage({
|
|
id: 'mobile.extension.file_error',
|
|
defaultMessage: 'There was an error reading the file to be shared.\nPlease try again.',
|
|
});
|
|
break;
|
|
}
|
|
let filename = fullPath.replace(/^.*[\\/]/, '');
|
|
let extension = filename.split('.').pop();
|
|
if (extension === filename) {
|
|
extension = getExtensionFromMime(item.type);
|
|
filename = `${filename}.${extension}`;
|
|
}
|
|
|
|
totalSize += fileSize.size;
|
|
files.push({
|
|
extension,
|
|
filename,
|
|
fullPath,
|
|
mimeType: item.type || lookupMimeType(filename.toLowerCase()),
|
|
size: getFormattedFileSize(fileSize),
|
|
type: item.type,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const value = text.join('\n');
|
|
|
|
if (!error && files.length <= MAX_FILE_COUNT && totalSize <= maxFileSize) {
|
|
this.props.navigation.setParams({
|
|
post: this.onPost,
|
|
});
|
|
}
|
|
|
|
this.setState({error, files, value, hasPermission: true, totalSize});
|
|
}
|
|
this.setState({loaded: true});
|
|
};
|
|
|
|
onClose = (data) => {
|
|
ShareExtension.close(data.nativeEvent ? null : data);
|
|
};
|
|
|
|
onPost = () => {
|
|
const {channelId, files, value} = this.state;
|
|
const {currentUserId, token, url} = this.props;
|
|
|
|
const data = {
|
|
channelId,
|
|
currentUserId,
|
|
files,
|
|
token,
|
|
url,
|
|
value,
|
|
};
|
|
|
|
this.onClose(data);
|
|
};
|
|
|
|
renderBody = () => {
|
|
const {formatMessage} = this.context.intl;
|
|
const {channelId, value} = this.state;
|
|
|
|
const channel = this.props.channels[channelId];
|
|
const channelDisplayName = channel?.display_name || ''; //eslint-disable-line camelcase
|
|
|
|
return (
|
|
<ScrollView
|
|
ref={this.getScrollViewRef}
|
|
contentContainerStyle={styles.scrollView}
|
|
style={styles.flex}
|
|
>
|
|
<TextInput
|
|
ref={this.getInputRef}
|
|
autoCapitalize='sentences'
|
|
maxLength={MAX_MESSAGE_LENGTH}
|
|
multiline={true}
|
|
onBlur={this.handleBlur}
|
|
onChangeText={this.handleTextChange}
|
|
onFocus={this.handleFocus}
|
|
placeholder={formatMessage({id: 'create_post.write', defaultMessage: 'Write to {channelDisplayName}'}, {channelDisplayName})}
|
|
placeholderTextColor={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
|
style={styles.input}
|
|
underlineColorAndroid='transparent'
|
|
value={value}
|
|
/>
|
|
{this.renderFiles()}
|
|
</ScrollView>
|
|
);
|
|
};
|
|
|
|
renderChannelButton = () => {
|
|
const {channelId} = this.state;
|
|
|
|
return (
|
|
<ChannelButton
|
|
channelId={channelId}
|
|
onPress={this.goToChannels}
|
|
theme={defaultTheme}
|
|
/>
|
|
);
|
|
};
|
|
|
|
renderErrorMessage = (message) => {
|
|
return (
|
|
<View
|
|
style={styles.flex}
|
|
>
|
|
<View style={styles.unauthenticatedContainer}>
|
|
<Text style={styles.unauthenticated}>
|
|
{message}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
renderFiles = () => {
|
|
const {files} = this.state;
|
|
|
|
return files.map((file, index) => {
|
|
let component;
|
|
|
|
if (file.type.startsWith('image')) {
|
|
component = (
|
|
<View
|
|
key={`item-${index}`}
|
|
style={styles.imageContainer}
|
|
>
|
|
<Image
|
|
source={{uri: file.fullPath, isStatic: true}}
|
|
resizeMode='cover'
|
|
style={styles.image}
|
|
/>
|
|
</View>
|
|
);
|
|
} else if (file.type.startsWith('video')) {
|
|
component = (
|
|
<View
|
|
key={`item-${index}`}
|
|
style={styles.imageContainer}
|
|
>
|
|
<Video
|
|
ref={`video-${index}`}
|
|
style={styles.video}
|
|
resizeMode='cover'
|
|
source={{uri: file.fullPath}}
|
|
volume={0}
|
|
paused={true}
|
|
onLoad={() => this.refs[`video-${index}`].seek(0)}
|
|
/>
|
|
</View>
|
|
);
|
|
} else {
|
|
let SvgIcon = extensionSvg[file.extension];
|
|
if (!SvgIcon) {
|
|
SvgIcon = GenericSvg;
|
|
}
|
|
|
|
component = (
|
|
<View
|
|
key={`item-${index}`}
|
|
style={styles.otherContainer}
|
|
>
|
|
<View style={styles.otherWrapper}>
|
|
<View style={styles.fileIcon}>
|
|
<SvgIcon
|
|
width={19}
|
|
height={48}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={styles.fileContainer}
|
|
key={`item-${index}`}
|
|
>
|
|
{component}
|
|
<Text
|
|
ellipsisMode='tail'
|
|
numberOfLines={1}
|
|
style={styles.filename}
|
|
>
|
|
{`${file.size} - ${file.filename}`}
|
|
</Text>
|
|
</View>
|
|
);
|
|
});
|
|
};
|
|
|
|
renderTeamButton = () => {
|
|
const {teamId} = this.state;
|
|
|
|
return (
|
|
<TeamButton
|
|
onPress={this.goToTeams}
|
|
teamId={teamId}
|
|
theme={defaultTheme}
|
|
/>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const {formatMessage} = this.context.intl;
|
|
const {maxFileSize, token, url} = this.props;
|
|
const {error, hasPermission, files, totalSize, loaded} = this.state;
|
|
|
|
if (!loaded) {
|
|
return (
|
|
<Loading/>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return this.renderErrorMessage(error);
|
|
}
|
|
|
|
if (token && url) {
|
|
if (hasPermission === false) {
|
|
const storage = formatMessage({
|
|
id: 'mobile.extension.permission',
|
|
defaultMessage: 'Mattermost needs access to the device storage to share files.',
|
|
});
|
|
|
|
return this.renderErrorMessage(storage);
|
|
} else if (files.length > MAX_FILE_COUNT) {
|
|
const fileCount = formatMessage({
|
|
id: 'mobile.extension.file_limit',
|
|
defaultMessage: 'Sharing is limited to a maximum of 5 files.',
|
|
});
|
|
|
|
return this.renderErrorMessage(fileCount);
|
|
} else if (totalSize > maxFileSize) {
|
|
const maxSize = formatMessage({
|
|
id: 'mobile.extension.max_file_size',
|
|
defaultMessage: 'File attachments shared in Mattermost must be less than {size}.',
|
|
}, {size: getFormattedFileSize({size: maxFileSize})});
|
|
|
|
return this.renderErrorMessage(maxSize);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.wrapper}>
|
|
{this.renderBody()}
|
|
<View style={styles.flex}>
|
|
{this.renderTeamButton()}
|
|
{this.renderChannelButton()}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const loginNeeded = formatMessage({
|
|
id: 'mobile.extension.authentication_required',
|
|
defaultMessage: 'Authentication required: Please first login using the app.',
|
|
});
|
|
|
|
return this.renderErrorMessage(loginNeeded);
|
|
}
|
|
}
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
return {
|
|
left: {
|
|
alignItems: 'center',
|
|
height: 50,
|
|
justifyContent: 'center',
|
|
width: 50,
|
|
},
|
|
closeButton: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
fontSize: 20,
|
|
},
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.centerChannelBg,
|
|
},
|
|
wrapper: {
|
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
|
flex: 1,
|
|
},
|
|
scrollView: {
|
|
padding: 15,
|
|
},
|
|
input: {
|
|
color: theme.centerChannelColor,
|
|
fontSize: 17,
|
|
height: INPUT_HEIGHT,
|
|
marginBottom: 5,
|
|
textAlignVertical: 'top',
|
|
width: '100%',
|
|
},
|
|
unauthenticatedContainer: {
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
paddingHorizontal: 20,
|
|
paddingTop: 35,
|
|
},
|
|
unauthenticated: {
|
|
color: theme.errorTextColor,
|
|
fontSize: 14,
|
|
},
|
|
fileContainer: {
|
|
alignItems: 'center',
|
|
backgroundColor: theme.centerChannelBg,
|
|
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
|
borderRadius: 4,
|
|
borderWidth: 1,
|
|
flexDirection: 'row',
|
|
height: 48,
|
|
marginBottom: 10,
|
|
width: '100%',
|
|
},
|
|
filename: {
|
|
color: changeOpacity(theme.centerChannelColor, 0.5),
|
|
fontSize: 13,
|
|
flex: 1,
|
|
},
|
|
otherContainer: {
|
|
borderBottomLeftRadius: 4,
|
|
borderTopLeftRadius: 4,
|
|
height: 48,
|
|
marginRight: 10,
|
|
paddingVertical: 10,
|
|
width: 38,
|
|
},
|
|
otherWrapper: {
|
|
borderRightWidth: 1,
|
|
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
|
|
flex: 1,
|
|
},
|
|
fileIcon: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flex: 1,
|
|
},
|
|
imageContainer: {
|
|
justifyContent: 'center',
|
|
borderBottomLeftRadius: 4,
|
|
borderTopLeftRadius: 4,
|
|
height: 48,
|
|
marginRight: 10,
|
|
width: 38,
|
|
overflow: 'hidden',
|
|
},
|
|
image: {
|
|
alignItems: 'center',
|
|
borderBottomLeftRadius: 4,
|
|
borderTopLeftRadius: 4,
|
|
height: 46,
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
width: 38,
|
|
},
|
|
video: {
|
|
alignItems: 'center',
|
|
height: 48,
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
width: 38,
|
|
},
|
|
};
|
|
});
|
|
|
|
const styles = getStyleSheet(defaultTheme);
|