Merge branch 'master' into mark-as-unread

This commit is contained in:
Harrison Healey 2019-11-18 17:21:42 -05:00
commit 725e9a6985
89 changed files with 3624 additions and 975 deletions

View file

@ -1,5 +1,37 @@
# Mattermost Mobile Apps Changelog
## 1.25.0 Release
- Release Date: November 16, 2019
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Compatibility
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where Mattermost monokai theme no longer worked properly on mobile apps.
- Fixed an issue on Android where the notification badge count didn't update when using multiple channels.
- Fixed an issue on Android where test notifications did not work properly.
- Fixed an issue where "In-app" notifications caused the app badge count to get out of sync.
- Fixed an issue on Android where email notification setting displayed was not updated when the setting was changed.
- Fixed an issue where Favorite channels list didn't update if the app was running in the background.
- Fixed an issue where the timezone setting did not update when changing it back to set automatically.
- Fixed an issue on iOS where clicking on a hashtag from "recent mentions" (or flagged posts) returned the user to the channel instead of displaying hashtag search results.
- Fixed an issue where tapping on a hashtag engaged a keyboard for a moment before displaying search results.
- Fixed an issue where posts of the same thread appeared to be from different threads if separated by a new message line.
- Fixed styling issues on iOS for Name, Purpose and Header information on the channel info screen.
- Fixed styling issues with bot posts timestamps in search results and pinned posts.
- Fixed styling issues on single sign-on screen in landscape view on iOS iPhone X and later.
- Fixed styling issues on iOS for the Helper text on Settings screens.
- Fixed an issue where the thread view header theme was inconsistent during transition back to main channel view.
- Fixed an issue on iOS where the navigation bar tucked under the phone's status bar when switching orientation.
- Fixed an issue on iOS where the keyboard flashed darker when Automatic Replies had been previously enabled.
- Fixed an issue on Android where uploading pictures from storage or camera required unwanted permissions.
- Fixed an issue where ``mobile.message_length.message`` did not match webapp's ``create_post.error_message``.
### Known Issues
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
## 1.24.0 Release
- Release Date: October 16, 2019
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device

View file

@ -268,6 +268,7 @@ dependencies {
implementation project(':react-native-android-open-settings')
implementation project(':react-native-haptic-feedback')
implementation project(':react-native-fast-image')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:2.0.0'
implementation 'com.facebook.fresco:animated-gif:2.0.0'

View file

@ -31,6 +31,7 @@ import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
import com.reactnativecommunity.netinfo.NetInfoModule;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import io.sentry.RNSentryModule;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.levelasquez.androidopensettings.AndroidOpenSettings;
import com.mkuczera.RNReactNativeHapticFeedbackModule;
@ -192,6 +193,7 @@ public class MainApplication extends NavigationApplication implements INotificat
};
}
},
new FastImageViewPackage(),
new RNCWebViewPackage(),
new SvgPackage(),
new LinearGradientPackage(),

View file

@ -3,6 +3,8 @@ include ':@sentry_react-native'
project(':@sentry_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android')
include ':react-native-android-open-settings'
project(':react-native-android-open-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-open-settings/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
include ':react-native-haptic-feedback'
project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
include ':react-native-gesture-handler'

View file

@ -14,6 +14,8 @@ import {
leaveChannel as serviceLeaveChannel,
selectChannel,
getChannelStats,
getChannels,
getArchivedChannels,
} from 'mattermost-redux/actions/channels';
import {
getPosts,
@ -73,6 +75,26 @@ export function loadChannelsByTeamName(teamName) {
};
}
export function loadPublicAndArchivedChannels(teamId, publicPage, archivedPage, perPage, shouldLoadArchivedChannels) {
return async (dispatch) => {
return dispatch(getChannels(
teamId,
publicPage,
perPage
)).then(async (publicChannels) => {
if (shouldLoadArchivedChannels) {
const archivedChannels = await dispatch(getArchivedChannels(
teamId,
archivedPage,
perPage
));
return archivedChannels;
}
return publicChannels;
});
};
}
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
return async (dispatch, getState) => {
const state = getState();

View file

@ -34,7 +34,12 @@ export default class AutocompleteSectionHeader extends PureComponent {
defaultMessage={defaultMessage}
style={style.sectionText}
/>
{loading && <ActivityIndicator size='small'/>}
{loading &&
<ActivityIndicator
color={theme.centerChannelColor}
size='small'
/>
}
</View>
</View>
);

View file

@ -20,7 +20,7 @@ function mapStateToProps(state, ownProps) {
let isBot = false;
let isGuest = false;
if (channel.type === General.DM_CHANNEL) {
if (channel?.type === General.DM_CHANNEL) {
const teammate = getUser(state, channel.teammate_id);
if (teammate) {
displayName = teammate.username;
@ -31,8 +31,8 @@ function mapStateToProps(state, ownProps) {
return {
displayName,
name: channel.name,
type: channel.type,
name: channel?.name,
type: channel?.type,
isBot,
isGuest,
theme: getTheme(state),

View file

@ -36,6 +36,7 @@ export default class AutocompleteSelector extends PureComponent {
errorText: PropTypes.node,
roundedBorders: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
disabled: PropTypes.bool,
};
static contextTypes = {
@ -119,6 +120,7 @@ export default class AutocompleteSelector extends PureComponent {
showRequiredAsterisk,
roundedBorders,
isLandscape,
disabled,
} = this.props;
const {selectedText} = this.state;
const style = getStyleSheet(theme);
@ -188,9 +190,10 @@ export default class AutocompleteSelector extends PureComponent {
{labelContent}
</View>
<TouchableWithFeedback
style={style.flex}
style={disabled ? style.disabled : null}
onPress={this.goToSelectorScreen}
type={'opacity'}
disabled={disabled}
>
<View style={inputStyle}>
<Text
@ -284,5 +287,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.errorTextColor,
fontSize: 14,
},
disabled: {
opacity: 0.5,
},
};
});

View file

@ -31,6 +31,16 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
onEndReachedThreshold={2}
onLayout={[Function]}
onScroll={[Function]}
refreshControl={
<RefreshControlMock
colors={
Array [
"#3d3c40",
]
}
tintColor="#3d3c40"
/>
}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}

View file

@ -15,6 +15,7 @@ import CustomListRow from 'app/components/custom_list/custom_list_row';
export default class ChannelListRow extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
isArchived: PropTypes.bool,
theme: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
...CustomListRow.propTypes,
@ -53,7 +54,7 @@ export default class ChannelListRow extends React.PureComponent {
<View style={style.container}>
<View style={style.titleContainer}>
<Icon
name='globe'
name={this.props.isArchived ? 'archive' : 'globe'}
style={style.icon}
/>
<Text style={style.displayName}>

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Keyboard, Platform, SectionList, Text, View} from 'react-native';
import {FlatList, Keyboard, Platform, RefreshControl, SectionList, Text, View} from 'react-native';
import {ListTypes} from 'app/constants';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@ -113,6 +113,14 @@ export default class CustomList extends PureComponent {
const {data, extraData, theme, onRefresh, refreshing} = this.props;
const style = getStyleFromTheme(theme);
const refreshControl = (
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[theme.centerChannelColor]}
tintColor={theme.centerChannelColor}
/>);
return (
<FlatList
contentContainerStyle={style.container}
@ -128,8 +136,7 @@ export default class CustomList extends PureComponent {
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
onLayout={this.handleLayout}
onScroll={this.handleScroll}
onRefresh={onRefresh}
refreshing={refreshing}
refreshControl={refreshControl}
ref={this.setListRef}
removeClippedSubviews={true}
renderItem={this.renderItem}

View file

@ -182,7 +182,7 @@ export default class EditChannelInfo extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -9,6 +9,7 @@ import {
StyleSheet,
Text,
} from 'react-native';
import FastImage from 'react-native-fast-image';
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
@ -140,7 +141,7 @@ export default class Emoji extends React.PureComponent {
}
return (
<Image
<FastImage
key={key}
style={{width, height}}
source={{uri: imageUrl}}

View file

@ -440,7 +440,7 @@ export default class EmojiPicker extends PureComponent {
const styles = getStyleSheetFromTheme(theme);
return (
<View style={styles.loading}>
<ActivityIndicator/>
<ActivityIndicator color={theme.centerChannelColor}/>
</View>
);
};

View file

@ -5,77 +5,70 @@ exports[`FileAttachment should match snapshot 1`] = `
style={
Array [
Object {
"borderColor": "rgba(61,60,64,0.2)",
"borderRadius": 2,
"borderColor": "rgba(61,60,64,0.4)",
"borderRadius": 5,
"borderWidth": 1,
"flex": 1,
"flexDirection": "row",
"marginRight": 10,
"marginTop": 10,
"width": 300,
},
]
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
type="opacity"
>
<FileAttachmentIcon
backgroundColor="#fff"
file={
Object {
"mime_type": "image/png",
}
}
iconHeight={60}
iconWidth={60}
onCaptureRef={[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",
}
}
wrapperHeight={80}
wrapperWidth={80}
/>
</TouchableWithFeedbackIOS>
<TouchableWithFeedbackIOS
onPress={[Function]}
<View
style={
Object {
"borderLeftColor": "rgba(61,60,64,0.2)",
"borderLeftWidth": 1,
"flex": 1,
"paddingHorizontal": 8,
"paddingVertical": 5,
"marginHorizontal": 20,
"marginVertical": 10,
}
}
type="opacity"
/>
>
<TouchableWithFeedbackIOS
onPress={[Function]}
type="opacity"
>
<FileAttachmentIcon
file={
Object {
"mime_type": "image/png",
}
}
iconHeight={48}
iconWidth={36}
onCaptureRef={[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",
}
}
wrapperHeight={48}
wrapperWidth={36}
/>
</TouchableWithFeedbackIOS>
</View>
</View>
`;

View file

@ -4,14 +4,18 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
PixelRatio,
Text,
View,
StyleSheet,
} from 'react-native';
import * as Utils from 'mattermost-redux/utils/file_utils.js';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isDocument, isGif} from 'app/utils/file';
import {calculateDimensions} from 'app/utils/images';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FileAttachmentDocument from './file_attachment_document';
@ -28,10 +32,14 @@ export default class FileAttachment extends PureComponent {
onLongPress: PropTypes.func,
onPreviewPress: PropTypes.func,
theme: PropTypes.object.isRequired,
wrapperWidth: PropTypes.number,
isSingleImage: PropTypes.bool,
nonVisibleImagesCount: PropTypes.number,
};
static defaultProps = {
onPreviewPress: () => true,
wrapperWidth: 300,
};
handleCaptureRef = (ref) => {
@ -51,7 +59,7 @@ export default class FileAttachment extends PureComponent {
};
renderFileInfo() {
const {file, theme} = this.props;
const {file, onLongPress, theme} = this.props;
const {data} = file;
const style = getStyleSheet(theme);
@ -60,24 +68,31 @@ export default class FileAttachment extends PureComponent {
}
return (
<View style={style.attachmentContainer}>
<Text
numberOfLines={2}
ellipsizeMode='tail'
style={style.fileName}
>
{file.caption.trim()}
</Text>
<View style={style.fileDownloadContainer}>
<TouchableWithFeedback
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
style={style.attachmentContainer}
>
<React.Fragment>
<Text
numberOfLines={2}
numberOfLines={1}
ellipsizeMode='tail'
style={style.fileInfo}
style={style.fileName}
>
{`${data.extension.toUpperCase()} ${Utils.getFormattedFileSize(data)}`}
{file.caption.trim()}
</Text>
</View>
</View>
<View style={style.fileDownloadContainer}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={style.fileInfo}
>
{`${Utils.getFormattedFileSize(data)}`}
</Text>
</View>
</React.Fragment>
</TouchableWithFeedback>
);
}
@ -85,75 +100,118 @@ export default class FileAttachment extends PureComponent {
this.documentElement = ref;
};
renderMoreImagesOverlay = (value) => {
if (!value) {
return null;
}
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.moreImagesWrapper}>
<Text style={style.moreImagesText}>
{`+${value}`}
</Text>
</View>
);
};
getImageDimensions = (file) => {
const {isSingleImage, wrapperWidth} = this.props;
const viewPortHeight = this.getViewPortHeight();
if (isSingleImage) {
return calculateDimensions(file?.height, file?.width, wrapperWidth, viewPortHeight);
}
return null;
};
getViewPortHeight = () => {
const dimensions = Dimensions.get('window');
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
return viewPortHeight;
};
render() {
const {
canDownloadFiles,
file,
theme,
onLongPress,
isSingleImage,
nonVisibleImagesCount,
} = this.props;
const {data} = file;
const style = getStyleSheet(theme);
let fileAttachmentComponent;
if ((data && data.has_preview_image) || file.loading || isGif(data)) {
const imageDimensions = this.getImageDimensions(data);
fileAttachmentComponent = (
<TouchableWithFeedback
key={`${this.props.id}${file.loading}`}
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
style={{width: imageDimensions?.width}}
>
<FileAttachmentImage
file={data || {}}
onCaptureRef={this.handleCaptureRef}
theme={theme}
isSingleImage={isSingleImage}
imageDimensions={imageDimensions}
/>
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
</TouchableWithFeedback>
);
} else if (isDocument(data)) {
fileAttachmentComponent = (
<FileAttachmentDocument
ref={this.setDocumentRef}
canDownloadFiles={canDownloadFiles}
file={file}
onLongPress={onLongPress}
theme={theme}
/>
<View style={[style.fileWrapper]}>
<View style={style.iconWrapper}>
<FileAttachmentDocument
ref={this.setDocumentRef}
canDownloadFiles={canDownloadFiles}
file={file}
onLongPress={onLongPress}
theme={theme}
/>
</View>
{this.renderFileInfo()}
</View>
);
} else {
fileAttachmentComponent = (
<TouchableWithFeedback
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
>
<FileAttachmentIcon
file={data}
onCaptureRef={this.handleCaptureRef}
theme={theme}
/>
</TouchableWithFeedback>
<View style={[style.fileWrapper]}>
<View style={style.iconWrapper}>
<TouchableWithFeedback
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
>
<FileAttachmentIcon
file={data}
onCaptureRef={this.handleCaptureRef}
theme={theme}
/>
</TouchableWithFeedback>
</View>
{this.renderFileInfo()}
</View>
);
}
return (
<View style={[style.fileWrapper]}>
{fileAttachmentComponent}
<TouchableWithFeedback
style={style.fileInfoContainer}
onLongPress={onLongPress}
onPress={this.handlePreviewPress}
type={'opacity'}
>
{this.renderFileInfo()}
</TouchableWithFeedback>
</View>
);
return fileAttachmentComponent;
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const scale = Dimensions.get('window').width / 320;
return {
attachmentContainer: {
flex: 1,
@ -168,33 +226,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 3,
},
fileInfo: {
marginLeft: 2,
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
fileInfoContainer: {
flex: 1,
paddingHorizontal: 8,
paddingVertical: 5,
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
color: theme.centerChannelColor,
},
fileName: {
flexDirection: 'column',
flexWrap: 'wrap',
marginLeft: 2,
fontSize: 14,
fontWeight: '600',
color: theme.centerChannelColor,
paddingRight: 10,
},
fileWrapper: {
flex: 1,
flexDirection: 'row',
marginTop: 10,
marginRight: 10,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRadius: 2,
width: 300,
borderColor: changeOpacity(theme.centerChannelColor, 0.4),
borderRadius: 5,
},
iconWrapper: {
marginHorizontal: 20,
marginVertical: 10,
},
circularProgress: {
width: '100%',
@ -211,5 +264,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
justifyContent: 'center',
},
moreImagesWrapper: {
...StyleSheet.absoluteFill,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
borderRadius: 5,
},
moreImagesText: {
color: theme.sidebarHeaderTextColor,
fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale)),
fontFamily: 'Open Sans',
textAlign: 'center',
},
};
});

View file

@ -10,6 +10,7 @@ import {
Platform,
StatusBar,
StyleSheet,
Text,
View,
} from 'react-native';
import OpenFile from 'react-native-doc-viewer';
@ -27,8 +28,9 @@ import mattermostBucket from 'app/mattermost_bucket';
import {changeOpacity} from 'app/utils/theme';
import {goToScreen} from 'app/actions/navigation';
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
const {DOCUMENTS_PATH} = DeviceTypes;
const DOWNLOADING_OFFSET = 28;
const TEXT_PREVIEW_FORMATS = [
'application/json',
'application/x-x509-ca-cert',
@ -50,10 +52,10 @@ export default class FileAttachmentDocument extends PureComponent {
};
static defaultProps = {
iconHeight: 47,
iconWidth: 47,
wrapperHeight: 80,
wrapperWidth: 80,
iconHeight: ATTACHMENT_ICON_HEIGHT,
iconWidth: ATTACHMENT_ICON_WIDTH,
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
wrapperWidth: ATTACHMENT_ICON_WIDTH,
};
static contextTypes = {
@ -284,16 +286,6 @@ export default class FileAttachmentDocument extends PureComponent {
}
};
renderProgress = () => {
const {wrapperWidth} = this.props;
return (
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
{this.renderFileAttachmentIcon()}
</View>
);
};
showDownloadDisabledAlert = () => {
const {intl} = this.context;
@ -338,14 +330,6 @@ export default class FileAttachmentDocument extends PureComponent {
renderFileAttachmentIcon = () => {
const {backgroundColor, iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
const {downloading} = this.state;
let height = wrapperHeight;
let width = wrapperWidth;
if (downloading) {
height -= DOWNLOADING_OFFSET;
width -= DOWNLOADING_OFFSET;
}
return (
<FileAttachmentIcon
@ -354,29 +338,39 @@ export default class FileAttachmentDocument extends PureComponent {
theme={theme}
iconHeight={iconHeight}
iconWidth={iconWidth}
wrapperHeight={height}
wrapperWidth={width}
wrapperHeight={wrapperHeight}
wrapperWidth={wrapperWidth}
/>
);
}
render() {
const {onLongPress, theme, wrapperHeight} = this.props;
const {downloading, progress} = this.state;
renderDownloadProgres = () => {
const {theme} = this.props;
return (
<Text style={{fontSize: 10, color: theme.centerChannelColor, fontWeight: '600'}}>
{`${this.state.progress}%`}
</Text>
);
};
render() {
const {onLongPress, theme} = this.props;
const {downloading, progress} = this.state;
let fileAttachmentComponent;
if (downloading) {
fileAttachmentComponent = (
<CircularProgress
size={wrapperHeight}
fill={progress}
width={circularProgressWidth}
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColor={theme.linkColor}
rotation={0}
>
{this.renderProgress}
</CircularProgress>
<View style={[style.circularProgressContent]}>
<CircularProgress
size={40}
fill={progress}
width={circularProgressWidth}
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColor={theme.linkColor}
rotation={0}
>
{this.renderDownloadProgres}
</CircularProgress>
</View>
);
} else {
fileAttachmentComponent = this.renderFileAttachmentIcon();
@ -396,11 +390,9 @@ export default class FileAttachmentDocument extends PureComponent {
const style = StyleSheet.create({
circularProgressContent: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
left: -circularProgressWidth,
position: 'absolute',
top: 0,
left: -(circularProgressWidth - 2),
top: 4,
width: 36,
height: 48,
},
});

View file

@ -19,9 +19,13 @@ import imageIcon from 'assets/images/icons/image.png';
import patchIcon from 'assets/images/icons/patch.png';
import pdfIcon from 'assets/images/icons/pdf.png';
import pptIcon from 'assets/images/icons/ppt.png';
import textIcon from 'assets/images/icons/text.png';
import videoIcon from 'assets/images/icons/video.png';
import wordIcon from 'assets/images/icons/word.png';
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
import {changeOpacity} from 'app/utils/theme';
const ICON_PATH_FROM_FILE_TYPE = {
audio: audioIcon,
code: codeIcon,
@ -31,6 +35,7 @@ const ICON_PATH_FROM_FILE_TYPE = {
pdf: pdfIcon,
presentation: pptIcon,
spreadsheet: excelIcon,
text: textIcon,
video: videoIcon,
word: wordIcon,
};
@ -44,14 +49,14 @@ export default class FileAttachmentIcon extends PureComponent {
onCaptureRef: PropTypes.func,
wrapperHeight: PropTypes.number,
wrapperWidth: PropTypes.number,
theme: PropTypes.object,
};
static defaultProps = {
backgroundColor: '#fff',
iconHeight: 60,
iconWidth: 60,
wrapperHeight: 80,
wrapperWidth: 80,
iconHeight: ATTACHMENT_ICON_HEIGHT,
iconWidth: ATTACHMENT_ICON_WIDTH,
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
wrapperWidth: ATTACHMENT_ICON_WIDTH,
};
getFileIconPath(file) {
@ -68,16 +73,17 @@ export default class FileAttachmentIcon extends PureComponent {
};
render() {
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth} = this.props;
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth, theme} = this.props;
const source = this.getFileIconPath(file);
const bgColor = backgroundColor || theme.centerChannelBg || 'transparent';
return (
<View
ref={this.handleCaptureRef}
style={[styles.fileIconWrapper, {backgroundColor, height: wrapperHeight, width: wrapperWidth}]}
style={[styles.fileIconWrapper, {backgroundColor: bgColor, height: wrapperHeight, width: wrapperWidth}]}
>
<Image
style={[styles.icon, {height: iconHeight, width: iconWidth}]}
style={{maxHeight: iconHeight, maxWidth: iconWidth, tintColor: changeOpacity(theme.centerChannelColor, 20)}}
source={source}
/>
</View>
@ -89,12 +95,5 @@ const styles = StyleSheet.create({
fileIconWrapper: {
alignItems: 'center',
justifyContent: 'center',
borderTopLeftRadius: 2,
borderBottomLeftRadius: 2,
},
icon: {
borderTopLeftRadius: 2,
borderBottomLeftRadius: 2,
backgroundColor: '#fff',
},
});

View file

@ -15,9 +15,13 @@ import ProgressiveImage from 'app/components/progressive_image';
import {isGif} from 'app/utils/file';
import {emptyFunction} from 'app/utils/general';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity} from 'app/utils/theme';
import thumb from 'assets/images/thumb.png';
const SMALL_IMAGE_MAX_HEIGHT = 48;
const SMALL_IMAGE_MAX_WIDTH = 48;
const IMAGE_SIZE = {
Fullsize: 'fullsize',
Preview: 'preview',
@ -35,22 +39,18 @@ export default class FileAttachmentImage extends PureComponent {
]),
imageWidth: PropTypes.number,
onCaptureRef: PropTypes.func,
theme: PropTypes.object,
resizeMode: PropTypes.string,
resizeMethod: PropTypes.string,
wrapperHeight: PropTypes.number,
wrapperWidth: PropTypes.number,
isSingleImage: PropTypes.bool,
imageDimensions: PropTypes.object,
};
static defaultProps = {
fadeInOnLoad: false,
imageHeight: 80,
imageSize: IMAGE_SIZE.Preview,
imageWidth: 80,
loading: false,
resizeMode: 'cover',
resizeMethod: 'resize',
wrapperHeight: 80,
wrapperWidth: 80,
};
constructor(props) {
@ -72,15 +72,11 @@ export default class FileAttachmentImage extends PureComponent {
};
}
calculateNeededWidth = (height, width, newHeight) => {
const ratio = width / height;
let newWidth = newHeight * ratio;
if (newWidth < newHeight) {
newWidth = newHeight;
boxPlaceholder = () => {
if (this.props.isSingleImage) {
return null;
}
return newWidth;
return (<View style={style.boxPlaceholder}/>);
};
handleCaptureRef = (ref) => {
@ -91,27 +87,7 @@ export default class FileAttachmentImage extends PureComponent {
}
};
render() {
const {
file,
imageHeight,
imageWidth,
imageSize,
resizeMethod,
resizeMode,
wrapperHeight,
wrapperWidth,
} = this.props;
let height = imageHeight;
let width = imageWidth;
let imageStyle = {height, width};
if (imageSize === IMAGE_SIZE.Preview) {
height = 80;
width = this.calculateNeededWidth(file.height, file.width, height) || 80;
imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2};
}
imageProps = (file) => {
const imageProps = {};
if (file.localPath) {
imageProps.defaultSource = {uri: file.localPath};
@ -119,20 +95,73 @@ export default class FileAttachmentImage extends PureComponent {
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
}
return imageProps;
};
renderSmallImage = () => {
const {file, isSingleImage, resizeMethod, theme} = this.props;
let wrapperStyle = style.fileImageWrapper;
if (isSingleImage) {
wrapperStyle = style.singleSmallImageWrapper;
if (file.width > SMALL_IMAGE_MAX_WIDTH) {
wrapperStyle = [wrapperStyle, {width: '100%'}];
}
}
return (
<View
ref={this.handleCaptureRef}
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}
style={[
wrapperStyle,
style.smallImageBorder,
{borderColor: changeOpacity(theme.centerChannelColor, 0.4)},
]}
>
{this.boxPlaceholder()}
<View style={style.smallImageOverlay}>
<ProgressiveImage
style={{height: file.height, width: file.width}}
defaultSource={thumb}
tintDefaultSource={!file.localPath}
filename={file.name}
resizeMode={'contain'}
resizeMethod={resizeMethod}
{...this.imageProps(file)}
/>
</View>
</View>
);
};
render() {
const {
file,
imageDimensions,
resizeMethod,
resizeMode,
} = this.props;
if (file.height <= SMALL_IMAGE_MAX_HEIGHT || file.width <= SMALL_IMAGE_MAX_WIDTH) {
return this.renderSmallImage();
}
return (
<View
ref={this.handleCaptureRef}
style={style.fileImageWrapper}
>
{this.boxPlaceholder()}
<ProgressiveImage
style={imageStyle}
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
defaultSource={thumb}
tintDefaultSource={!file.localPath}
filename={file.name}
resizeMode={resizeMode}
resizeMethod={resizeMethod}
{...imageProps}
{...this.imageProps(file)}
/>
</View>
);
@ -140,17 +169,28 @@ export default class FileAttachmentImage extends PureComponent {
}
const style = StyleSheet.create({
fileImageWrapper: {
alignItems: 'center',
justifyContent: 'center',
borderBottomLeftRadius: 2,
borderTopLeftRadius: 2,
imagePreview: {
...StyleSheet.absoluteFill,
},
loaderContainer: {
position: 'absolute',
height: '100%',
width: '100%',
alignItems: 'center',
fileImageWrapper: {
borderRadius: 5,
overflow: 'hidden',
},
boxPlaceholder: {
paddingBottom: '100%',
},
smallImageBorder: {
borderWidth: 1,
borderRadius: 5,
},
smallImageOverlay: {
...StyleSheet.absoluteFill,
justifyContent: 'center',
alignItems: 'center',
},
singleSmallImageWrapper: {
height: SMALL_IMAGE_MAX_HEIGHT,
width: SMALL_IMAGE_MAX_WIDTH,
overflow: 'hidden',
},
});

View file

@ -1,15 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ScrollView,
StyleSheet,
} from 'react-native';
import {Dimensions, StyleSheet, View} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
import {DeviceTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import {isDocument, isGif, isVideo} from 'app/utils/file';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {previewImageAtIndex} from 'app/utils/images';
@ -18,7 +20,11 @@ import {emptyFunction} from 'app/utils/general';
import FileAttachment from './file_attachment';
export default class FileAttachmentList extends Component {
const MAX_VISIBLE_ROW_IMAGES = 4;
const VIEWPORT_IMAGE_OFFSET = 70;
const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
export default class FileAttachmentList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
@ -30,6 +36,7 @@ export default class FileAttachmentList extends Component {
onLongPress: PropTypes.func,
postId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
isReplyPost: PropTypes.bool,
};
static defaultProps = {
@ -40,19 +47,25 @@ export default class FileAttachmentList extends Component {
super(props);
this.items = [];
this.previewItems = [];
this.filesForGallery = this.getFilesForGallery(props);
this.state = {
loadingFiles: props.files.length === 0,
};
this.buildGalleryFiles(props).then((results) => {
this.buildGalleryFiles().then((results) => {
this.galleryFiles = results;
});
}
componentDidMount() {
const {files} = this.props;
this.mounted = true;
this.handlePermanentSidebar();
this.handleDimensions();
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
Dimensions.addEventListener('change', this.handleDimensions);
if (files.length === 0) {
this.loadFilesForPost();
}
@ -60,7 +73,8 @@ export default class FileAttachmentList extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.files !== nextProps.files) {
this.buildGalleryFiles(nextProps).then((results) => {
this.filesForGallery = this.getFilesForGallery(nextProps);
this.buildGalleryFiles().then((results) => {
this.galleryFiles = results;
});
}
@ -72,20 +86,33 @@ export default class FileAttachmentList extends Component {
}
}
loadFilesForPost = async () => {
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
this.setState({
loadingFiles: false,
});
componentWillUnmount() {
this.mounted = false;
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
Dimensions.removeEventListener('change', this.handleDimensions);
}
buildGalleryFiles = async (props) => {
const {files} = props;
attachmentIndex = (fileId) => {
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
};
attachmentManifest = (attachments) => {
return attachments.reduce((info, file) => {
if (this.isImage(file)) {
info.imageAttachments.push(file);
} else {
info.nonImageAttachments.push(file);
}
return info;
}, {imageAttachments: [], nonImageAttachments: []});
};
buildGalleryFiles = async () => {
const results = [];
if (files && files.length) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (this.filesForGallery && this.filesForGallery.length) {
for (let i = 0; i < this.filesForGallery.length; i++) {
const file = this.filesForGallery[i];
const caption = file.name;
if (isDocument(file) || isVideo(file) || (!file.has_preview_image && !isGif(file))) {
@ -116,16 +143,141 @@ export default class FileAttachmentList extends Component {
return results;
};
getFilesForGallery = (props) => {
const manifest = this.attachmentManifest(props.files);
const files = manifest.imageAttachments.concat(manifest.nonImageAttachments);
const results = [];
if (files && files.length) {
files.forEach((file) => {
results.push(file);
});
}
return results;
};
getPortraitPostWidth = () => {
const {isReplyPost} = this.props;
const {width, height} = Dimensions.get('window');
const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
if (permanentSidebar) {
portraitPostWidth -= TABLET_WIDTH;
}
if (isReplyPost) {
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
}
return portraitPostWidth;
};
handleCaptureRef = (ref, idx) => {
this.items[idx] = ref;
};
handleDimensions = () => {
if (this.mounted) {
if (DeviceTypes.IS_TABLET) {
mattermostManaged.isRunningInSplitView().then((result) => {
const isSplitView = Boolean(result.isSplitView);
this.setState({isSplitView});
});
}
}
};
handlePermanentSidebar = async () => {
if (DeviceTypes.IS_TABLET && this.mounted) {
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
this.setState({permanentSidebar: enabled === 'true'});
}
};
handlePreviewPress = preventDoubleTap((idx) => {
previewImageAtIndex(this.items, idx, this.galleryFiles);
});
renderItems = () => {
const {canDownloadFiles, fileIds, files} = this.props;
isImage = (file) => (file.has_preview_image || isGif(file));
isSingleImage = (files) => (files.length === 1 && this.isImage(files[0]));
loadFilesForPost = async () => {
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
this.setState({
loadingFiles: false,
});
}
renderItems = (items, moreImagesCount, includeGutter = false) => {
const {canDownloadFiles, onLongPress, theme} = this.props;
const isSingleImage = this.isSingleImage(items);
let nonVisibleImagesCount;
let container = styles.container;
const containerWithGutter = [container, styles.gutter];
return items.map((file, idx) => {
const f = {
caption: file.name,
data: file,
};
if (moreImagesCount && idx === MAX_VISIBLE_ROW_IMAGES - 1) {
nonVisibleImagesCount = moreImagesCount;
}
if (idx !== 0 && includeGutter) {
container = containerWithGutter;
}
return (
<View
style={container}
key={file.id}
>
<FileAttachment
key={file.id}
canDownloadFiles={canDownloadFiles}
file={f}
id={file.id}
index={this.attachmentIndex(file.id)}
onCaptureRef={this.handleCaptureRef}
onPreviewPress={this.handlePreviewPress}
onLongPress={onLongPress}
theme={theme}
isSingleImage={isSingleImage}
nonVisibleImagesCount={nonVisibleImagesCount}
wrapperWidth={this.getPortraitPostWidth()}
/>
</View>
);
});
};
renderImageRow = (images) => {
if (images.length === 0) {
return null;
}
const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES);
const {portraitPostWidth} = this.state;
let nonVisibleImagesCount;
if (images.length > MAX_VISIBLE_ROW_IMAGES) {
nonVisibleImagesCount = images.length - MAX_VISIBLE_ROW_IMAGES;
}
return (
<View style={[styles.row, {width: portraitPostWidth}]}>
{ this.renderItems(visibleImages, nonVisibleImagesCount, true) }
</View>
);
};
render() {
const {canDownloadFiles, fileIds, files, isFailed} = this.props;
if (!files.length && fileIds.length > 0) {
return fileIds.map((id, idx) => (
@ -140,45 +292,29 @@ export default class FileAttachmentList extends Component {
));
}
return files.map((file, idx) => {
const f = {
caption: file.name,
data: file,
};
return (
<FileAttachment
key={file.id}
canDownloadFiles={canDownloadFiles}
file={f}
id={file.id}
index={idx}
onCaptureRef={this.handleCaptureRef}
onPreviewPress={this.handlePreviewPress}
onLongPress={this.props.onLongPress}
theme={this.props.theme}
/>
);
});
};
render() {
const {fileIds, isFailed} = this.props;
const manifest = this.attachmentManifest(files);
return (
<ScrollView
horizontal={true}
scrollEnabled={fileIds.length > 1}
style={[(isFailed && styles.failed)]}
keyboardShouldPersistTaps={'always'}
>
{this.renderItems()}
</ScrollView>
<View style={[isFailed && styles.failed]}>
{this.renderImageRow(manifest.imageAttachments)}
{this.renderItems(manifest.nonImageAttachments)}
</View>
);
}
}
const styles = StyleSheet.create({
row: {
flex: 1,
flexDirection: 'row',
marginTop: 5,
},
container: {
flex: 1,
},
gutter: {
marginLeft: 8,
},
failed: {
opacity: 0.5,
},

View file

@ -10,9 +10,50 @@ jest.mock('react-native-doc-viewer', () => ({
openDoc: jest.fn(),
}));
describe('PostAttachmentOpenGraph', () => {
describe('FileAttachmentList', () => {
const loadFilesForPostIfNecessary = jest.fn().mockImplementationOnce(() => Promise.resolve({data: {}}));
const files = [{
create_at: 1546893090093,
delete_at: 0,
extension: 'png',
has_preview_image: true,
height: 171,
id: 'fileId',
mime_type: 'image/png',
name: 'image01.png',
post_id: 'postId',
size: 14894,
update_at: 1546893090093,
user_id: 'userId',
width: 425,
},
{
create_at: 1546893090093,
delete_at: 0,
extension: 'png',
has_preview_image: true,
height: 800,
id: 'otherFileId',
mime_type: 'image/png',
name: 'image02.png',
post_id: 'postId',
size: 24894,
update_at: 1546893090093,
user_id: 'userId',
width: 555,
}];
const nonImage = {
extension: 'other',
id: 'fileId',
mime_type: 'other/type',
name: 'file01.other',
post_id: 'postId',
size: 14894,
user_id: 'userId',
};
const baseProps = {
actions: {
loadFilesForPostIfNecessary,
@ -21,21 +62,7 @@ describe('PostAttachmentOpenGraph', () => {
deviceHeight: 680,
deviceWidth: 660,
fileIds: ['fileId'],
files: [{
create_at: 1546893090093,
delete_at: 0,
extension: 'png',
has_preview_image: true,
height: 171,
id: 'fileId',
mime_type: 'image/png',
name: 'image.png',
post_id: 'postId',
size: 14894,
update_at: 1546893090093,
user_id: 'userId',
width: 425,
}],
files: [files[0]],
postId: 'postId',
theme: Preferences.THEMES.default,
};
@ -48,6 +75,93 @@ describe('PostAttachmentOpenGraph', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with two image files', () => {
const props = {
...baseProps,
files,
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with three image files', () => {
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
const props = {
...baseProps,
files: [...files, thirdImage],
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with four image files', () => {
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
const props = {
...baseProps,
files: [...files, thirdImage, fourthImage],
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with more than four image files', () => {
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
const fifthImage = {...files[1], id: 'fifthFileId', name: 'image05.png'};
const sixthImage = {...files[1], id: 'sixthFileId', name: 'image06.png'};
const props = {
...baseProps,
files: [...files, thirdImage, fourthImage, fifthImage, sixthImage],
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with non-image attachment', () => {
const props = {
...baseProps,
files: [nonImage],
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with combination of image and non-image attachments', () => {
const props = {
...baseProps,
files: [...files, nonImage],
};
const wrapper = shallow(
<FileAttachment {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call loadFilesForPostIfNecessary when files does not exist', async () => {
const props = {
...baseProps,

View file

@ -190,11 +190,7 @@ export default class FileUploadItem extends PureComponent {
filePreviewComponent = (
<FileAttachmentImage
file={file}
imageSize='fullsize'
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
theme={theme}
/>
);
} else {
@ -202,8 +198,6 @@ export default class FileUploadItem extends PureComponent {
<FileAttachmentIcon
file={file}
theme={theme}
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
/>
@ -260,6 +254,7 @@ const styles = StyleSheet.create({
height: 100,
width: 100,
elevation: 10,
borderRadius: 5,
...Platform.select({
ios: {
backgroundColor: '#fff',

View file

@ -46,7 +46,7 @@ export default class LoadMorePosts extends PureComponent {
return (
<View style={{flex: 1, alignItems: 'center'}}>
<ActivityIndicator/>
<ActivityIndicator color={this.props.theme.centerChannelColor}/>
</View>
);
}

View file

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import Button from 'react-native-button';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import ActionButtonText from './action_button_text';
export default class ActionButton extends PureComponent {
@ -19,6 +19,7 @@ export default class ActionButton extends PureComponent {
postId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
cookie: PropTypes.string.isRequired,
disabled: PropTypes.bool,
};
handleActionPress = preventDoubleTap(() => {
@ -27,13 +28,15 @@ export default class ActionButton extends PureComponent {
}, 4000);
render() {
const {name, theme} = this.props;
const {name, theme, disabled} = this.props;
const style = getStyleSheet(theme);
return (
<Button
containerStyle={style.button}
disabledContainerStyle={style.buttonDisabled}
onPress={this.handleActionPress}
disabled={disabled}
>
<ActionButtonText
message={name}
@ -49,6 +52,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
button: {
borderRadius: 2,
backgroundColor: theme.buttonBg,
opacity: 1,
alignItems: 'center',
marginBottom: 2,
marginRight: 5,
@ -56,6 +60,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingHorizontal: 10,
paddingVertical: 7,
},
buttonDisabled: {
backgroundColor: changeOpacity(theme.buttonBg, 0.3),
},
text: {
color: theme.buttonColor,
fontSize: 12,

View file

@ -18,6 +18,7 @@ export default class ActionMenu extends PureComponent {
options: PropTypes.arrayOf(PropTypes.object),
postId: PropTypes.string.isRequired,
selected: PropTypes.object,
disabled: PropTypes.bool,
};
constructor(props) {
@ -62,6 +63,7 @@ export default class ActionMenu extends PureComponent {
name,
dataSource,
options,
disabled,
} = this.props;
const {selected} = this.state;
@ -72,6 +74,7 @@ export default class ActionMenu extends PureComponent {
options={options}
selected={selected}
onSelected={this.handleSelect}
disabled={disabled}
/>
);
}

View file

@ -52,4 +52,14 @@ describe('ActionMenu', () => {
expect(wrapper.state('selected')).toBe(props.selected);
});
test('disabled works', () => {
const props = {
...baseProps,
disabled: true,
};
const wrapper = shallow(<ActionMenu {...props}/>);
expect(wrapper.props().disabled).toBe(true);
});
});

View file

@ -41,6 +41,7 @@ export default class AttachmentActions extends PureComponent {
defaultOption={action.default_option}
options={action.options}
postId={postId}
disabled={action.disabled}
/>
);
break;
@ -53,6 +54,7 @@ export default class AttachmentActions extends PureComponent {
cookie={action.cookie}
name={action.name}
postId={postId}
disabled={action.disabled}
/>
);
break;

View file

@ -32,6 +32,14 @@ exports[`AttachmentImage it matches snapshot 1`] = `
}
>
<Connect(ProgressiveImage)
imageStyle={
Object {
"marginBottom": 5,
"marginLeft": 2.5,
"marginRight": 5,
"marginTop": 2.5,
}
}
imageUri="https://images.com/image.png"
resizeMode="contain"
style={

View file

@ -141,6 +141,7 @@ export default class AttachmentImage extends PureComponent {
progressiveImage = (
<ProgressiveImage
ref={this.setImageRef}
imageStyle={style.attachmentMargin}
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
@ -178,5 +179,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderRadius: 2,
flex: 1,
},
attachmentMargin: {
marginTop: 2.5,
marginLeft: 2.5,
marginBottom: 5,
marginRight: 5,
},
};
});

View file

@ -2,8 +2,9 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Image, StyleSheet, View} from 'react-native';
import {StyleSheet, View} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
export default class AttachmentThumbnail extends PureComponent {
static propTypes = {
@ -19,7 +20,7 @@ export default class AttachmentThumbnail extends PureComponent {
return (
<View style={style.container}>
<Image
<FastImage
source={{uri}}
resizeMode='contain'
resizeMethod='scale'

View file

@ -254,6 +254,7 @@ export default class Post extends PureComponent {
isLandscape,
previousPostExists,
beforePrevPostUserId,
isFirstReply,
} = this.props;
if (!post) {
@ -305,6 +306,7 @@ export default class Post extends PureComponent {
theme={theme}
previousPostExists={previousPostExists}
beforePrevPostUserId={beforePrevPostUserId}
isFirstReply={isFirstReply}
/>
);
}

View file

@ -247,6 +247,7 @@ export default class PostBody extends PureComponent {
isFailed={isFailed}
onLongPress={this.showPostOptions}
postId={post.id}
isReplyPost={this.props.isReplyPost}
/>
);
}

View file

@ -244,6 +244,76 @@ exports[`PostHeader should match snapshot when post is from system message 1`] =
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post is same thread, so dont display Commented On 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
null,
]
}
type="opacity"
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
</View>
</View>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyButton 1`] = `
<React.Fragment>
<View
@ -396,6 +466,93 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post renders Commented On for new post 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
null,
]
}
type="opacity"
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
</View>
</View>
<FormattedText
defaultMessage="Commented on {name}{apostrophe} message: "
id="post_body.commentedOn"
style={
Object {
"color": "rgba(61,60,64,0.65)",
"lineHeight": 21,
"marginBottom": 3,
}
}
values={
Object {
"apostrophe": "'s",
"name": "John Doe",
}
}
/>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post should display reply button 1`] = `
<React.Fragment>
<View

View file

@ -46,6 +46,7 @@ export default class PostHeader extends PureComponent {
previousPostExists: PropTypes.bool,
post: PropTypes.object,
beforePrevPostUserId: PropTypes.string,
isFirstReply: PropTypes.bool,
};
static defaultProps = {
@ -68,8 +69,9 @@ export default class PostHeader extends PureComponent {
previousPostExists,
renderReplies,
theme,
isFirstReply,
} = this.props;
if (!renderReplies || !commentedOnDisplayName || (!previousPostExists && post.user_id === beforePrevPostUserId)) {
if (!isFirstReply || !renderReplies || !commentedOnDisplayName || (!previousPostExists && post.user_id === beforePrevPostUserId)) {
return null;
}

View file

@ -35,6 +35,7 @@ describe('PostHeader', () => {
post: {id: 'post'},
beforePrevPostUserId: '0',
onPress: jest.fn(),
isFirstReply: true,
};
test('should match snapshot when just a base post', () => {
@ -95,4 +96,34 @@ describe('PostHeader', () => {
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
});
test('should match snapshot when post renders Commented On for new post', () => {
const props = {
...baseProps,
isFirstReply: true,
renderReplies: true,
commentedOnDisplayName: 'John Doe',
previousPostExists: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when post is same thread, so dont display Commented On', () => {
const props = {
...baseProps,
isFirstReply: false,
renderReplies: true,
commentedOnDisplayName: 'John Doe',
previousPostExists: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -41,7 +41,18 @@ exports[`PostList setting channel deep link 1`] = `
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
refreshControl={
<RefreshControlMock
colors={
Array [
"#3d3c40",
]
}
onRefresh={null}
refreshing={false}
tintColor="#3d3c40"
/>
}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
@ -91,7 +102,18 @@ exports[`PostList setting permalink deep link 1`] = `
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
refreshControl={
<RefreshControlMock
colors={
Array [
"#3d3c40",
]
}
onRefresh={null}
refreshing={false}
tintColor="#3d3c40"
/>
}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
@ -141,7 +163,18 @@ exports[`PostList should match snapshot 1`] = `
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
refreshControl={
<RefreshControlMock
colors={
Array [
"#3d3c40",
]
}
onRefresh={null}
refreshing={false}
tintColor="#3d3c40"
/>
}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, StyleSheet} from 'react-native';
import {FlatList, RefreshControl, StyleSheet} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import * as PostListUtils from 'mattermost-redux/utils/post_list';
@ -366,13 +366,16 @@ export default class PostList extends PureComponent {
postIds,
refreshing,
scrollViewNativeID,
theme,
} = this.props;
const refreshControl = {refreshing};
if (channelId) {
refreshControl.onRefresh = this.handleRefresh;
}
const refreshControl = (
<RefreshControl
refreshing={refreshing}
onRefresh={channelId ? this.handleRefresh : null}
colors={[theme.centerChannelColor]}
tintColor={theme.centerChannelColor}
/>);
const hasPostsKey = postIds.length ? 'true' : 'false';
@ -398,7 +401,7 @@ export default class PostList extends PureComponent {
removeClippedSubviews={true}
renderItem={this.renderItem}
scrollEventThrottle={60}
{...refreshControl}
refreshControl={refreshControl}
nativeID={scrollViewNativeID}
/>
);

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgressiveImage should match snapshot when just a image loads 1`] = `
<View
style={Object {}}
/>
`;
exports[`ProgressiveImage should match snapshot when no thumbnail 1`] = `
<View
style={Object {}}
/>
`;

View file

@ -4,12 +4,14 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native';
import FastImage from 'react-native-fast-image';
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity} from 'app/utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground);
const AnimatedImage = Animated.createAnimatedComponent(FastImage);
export default class ProgressiveImage extends PureComponent {
static propTypes = {
@ -18,6 +20,7 @@ export default class ProgressiveImage extends PureComponent {
defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component
filename: PropTypes.string,
imageUri: PropTypes.string,
imageStyle: CustomPropTypes.Style,
onError: PropTypes.func,
resizeMethod: PropTypes.string,
resizeMode: PropTypes.string,
@ -40,6 +43,7 @@ export default class ProgressiveImage extends PureComponent {
intensity: new Animated.Value(80),
thumb: null,
uri: null,
failedImageLoad: false,
};
}
@ -86,17 +90,32 @@ export default class ProgressiveImage extends PureComponent {
setThumbnail = (thumb) => {
if (this.subscribedToCache) {
const {filename, imageUri} = this.props;
this.setState({thumb}, () => {
setTimeout(() => {
ImageCacheManager.cache(filename, imageUri, this.setImage);
}, 300);
});
if (!thumb && !this.state.failedImageLoad) {
this.load();
this.setState({failedImageLoad: true});
} else {
const {filename, imageUri} = this.props;
this.setState({thumb}, () => {
setTimeout(() => {
ImageCacheManager.cache(filename, imageUri, this.setImage);
}, 300);
});
}
}
};
render() {
const {style, defaultSource, isBackgroundImage, theme, tintDefaultSource, onError, resizeMode, resizeMethod} = this.props;
const {
defaultSource,
imageStyle,
isBackgroundImage,
onError,
resizeMode,
resizeMethod,
style,
theme,
tintDefaultSource,
} = this.props;
const {uri, intensity, thumb} = this.state;
const hasDefaultSource = Boolean(defaultSource);
const hasPreview = Boolean(thumb);
@ -114,16 +133,18 @@ export default class ProgressiveImage extends PureComponent {
ImageComponent = AnimatedImageBackground;
} else {
DefaultComponent = Image;
ImageComponent = Animated.Image;
ImageComponent = AnimatedImage;
}
const styles = getStyleSheet(theme);
let defaultImage;
if (hasDefaultSource && tintDefaultSource) {
defaultImage = (
<View style={styles.defaultImageContainer}>
<DefaultComponent
source={defaultSource}
style={{flex: 1, tintColor: changeOpacity(theme.centerChannelColor, 0.2)}}
style={styles.defaultImageTint}
resizeMode='center'
resizeMethod={resizeMethod}
onError={onError}
@ -139,7 +160,7 @@ export default class ProgressiveImage extends PureComponent {
resizeMethod={resizeMethod}
onError={onError}
source={defaultSource}
style={StyleSheet.absoluteFill}
style={[StyleSheet.absoluteFill, imageStyle]}
>
{this.props.children}
</DefaultComponent>
@ -155,7 +176,7 @@ export default class ProgressiveImage extends PureComponent {
resizeMethod={resizeMethod}
onError={onError}
source={{uri: thumb}}
style={StyleSheet.absoluteFill}
style={[StyleSheet.absoluteFill, imageStyle]}
blurRadius={5}
>
{this.props.children}
@ -167,7 +188,7 @@ export default class ProgressiveImage extends PureComponent {
resizeMethod={resizeMethod}
onError={onError}
source={{uri}}
style={[StyleSheet.absoluteFill, styles.attachmentMargin]}
style={[StyleSheet.absoluteFill, imageStyle]}
>
{this.props.children}
</ImageComponent>
@ -180,19 +201,19 @@ export default class ProgressiveImage extends PureComponent {
}
}
const styles = StyleSheet.create({
defaultImageContainer: {
flex: 1,
position: 'absolute',
height: 80,
width: 80,
alignItems: 'center',
justifyContent: 'center',
},
attachmentMargin: {
marginTop: 2.5,
marginLeft: 2.5,
marginBottom: 5,
marginRight: 5,
},
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
defaultImageContainer: {
flex: 1,
position: 'absolute',
height: 80,
width: 80,
alignItems: 'center',
justifyContent: 'center',
},
defaultImageTint: {
flex: 1,
tintColor: changeOpacity(theme.centerChannelColor, 0.2),
},
};
});

View file

@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import ProgressiveImage from './progressive_image';
describe('ProgressiveImage', () => {
const baseProps = {
isBackgroundImage: false,
defaultSource: 0,
filename: 'defaultImage',
imageUri: 'https://images.com/image.png',
onError: jest.fn(),
resizeMethod: 'auto',
resizeMode: 'contain',
theme: Preferences.THEMES.default,
thumbnailUri: 'https://images.com/image.png',
tintDefaultSource: false,
};
test('should match snapshot when just a image loads', () => {
const wrapper = shallow(
<ProgressiveImage {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when no thumbnail', () => {
const props = {
...baseProps,
thumbnailUri: null,
};
const wrapper = shallow(
<ProgressiveImage {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -86,7 +86,7 @@ export default class SafeAreaIos extends PureComponent {
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
const window = dimensions?.window || Dimensions.get('window');
const orientation = window.width > window.length ? LANDSCAPE : PORTRAIT;
const orientation = window.width > window.height ? LANDSCAPE : PORTRAIT;
const {safeAreaInsets} = await SafeArea.getSafeAreaInsetsForRootView();
this.setSafeAreaInsets(safeAreaInsets, orientation);
}

View file

@ -11,6 +11,7 @@ import {
Easing,
Keyboard,
PanResponder,
Platform,
StyleSheet,
TouchableWithoutFeedback,
View,
@ -119,6 +120,16 @@ export default class DrawerLayout extends Component {
componentDidMount() {
Dimensions.addEventListener('change', this.handleDimensionsChange);
if (Platform.OS === 'ios') {
// on iOS force closing the drawers to prevent them for partially showing
// when channging the device orientation, probably caused by RN61
Animated.timing(this.openValue, {
toValue: 0,
duration: 10,
easing: Easing.out(Easing.cubic),
useNativeDriver: this.props.useNativeAnimations,
}).start();
}
}
componentWillUnmount() {
@ -263,7 +274,6 @@ export default class DrawerLayout extends Component {
const {isTablet} = this.props;
const panHandlers = isTablet ? emptyObject : this._panResponder.panHandlers;
const containerStyles = [styles.container];
if (isTablet) {
containerStyles.push(styles.tabletContainer);
}

View file

@ -195,7 +195,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: ANDROID_TOP_PORTRAIT,
},
ios: {
height: 44,
height: 54,
},
}),
},

View file

@ -2,3 +2,5 @@
// See LICENSE.txt for license information.
export const MAX_ATTACHMENT_FOOTER_LENGTH = 300;
export const ATTACHMENT_ICON_HEIGHT = 48;
export const ATTACHMENT_ICON_WIDTH = 36;

View file

@ -292,7 +292,10 @@ export default class ChannelBase extends PureComponent {
theme={theme}
isLandscape={isLandscape}
/>
<Loading channelIsLoading={true}/>
<Loading
channelIsLoading={true}
color={theme.centerChannelColor}
/>
</View>
</SafeAreaView>
);

View file

@ -303,7 +303,7 @@ export default class ChannelAddMembers extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -335,7 +335,7 @@ export default class ChannelMembers extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -173,7 +173,7 @@ export default class EditPost extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -560,7 +560,7 @@ export default class EditProfile extends PureComponent {
return (
<View style={[style.container, style.flex]}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -78,7 +78,7 @@ export default class ErrorTeamsList extends PureComponent {
const style = getStyleFromTheme(theme);
if (this.state.loading) {
return <Loading/>;
return <Loading color={theme.centerChannelColor}/>;
}
return (

View file

@ -151,6 +151,7 @@ exports[`ForgotPassword should match snapshot 1`] = `
"borderColor": "gainsboro",
"borderRadius": 3,
"borderWidth": 1,
"color": "#3d3c40",
"fontSize": 16,
"height": 45,
"marginBottom": 5,
@ -262,6 +263,7 @@ exports[`ForgotPassword snapshot for error on failure of email regex 1`] = `
"borderColor": "gainsboro",
"borderRadius": 3,
"borderWidth": 1,
"color": "#3d3c40",
"fontSize": 16,
"height": 45,
"marginBottom": 5,

View file

@ -3,11 +3,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, Animated, CameraRoll, InteractionManager, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import {Alert, Animated, InteractionManager, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import {CircularProgress} from 'react-native-circular-progress';
import Icon from 'react-native-vector-icons/Ionicons';
import {intlShape} from 'react-intl';
import CameraRoll from '@react-native-community/cameraroll';
import {Client4} from 'mattermost-redux/client';

View file

@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Alert,
Animated,
Image,
Platform,
SafeAreaView,
StatusBar,
@ -22,6 +21,7 @@ import {intlShape} from 'react-intl';
import Permissions from 'react-native-permissions';
import Gallery from 'react-native-image-gallery';
import DeviceInfo from 'react-native-device-info';
import FastImage from 'react-native-fast-image';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -206,8 +206,8 @@ export default class ImagePreview extends PureComponent {
backgroundColor='transparent'
canDownloadFiles={canDownloadFiles}
file={file}
iconHeight={100}
iconWidth={100}
iconHeight={200}
iconWidth={200}
theme={theme}
wrapperHeight={200}
wrapperWidth={200}
@ -223,8 +223,8 @@ export default class ImagePreview extends PureComponent {
backgroundColor='transparent'
file={file}
theme={this.props.theme}
iconHeight={150}
iconWidth={150}
iconHeight={200}
iconWidth={200}
wrapperHeight={200}
wrapperWidth={200}
/>
@ -366,7 +366,7 @@ export default class ImagePreview extends PureComponent {
if (imageDimensions) {
const {deviceHeight, deviceWidth} = this.props;
const {height, width} = imageDimensions;
const {style, ...otherProps} = imageProps;
const {style, source} = imageProps;
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
const flattenStyle = StyleSheet.flatten(style);
const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
@ -374,8 +374,8 @@ export default class ImagePreview extends PureComponent {
return (
<View style={[style, {justifyContent: 'center', alignItems: 'center'}]}>
<Image
{...otherProps}
<FastImage
source={source}
style={imageStyle}
/>
</View>

View file

@ -43,6 +43,35 @@ exports[`MoreChannels should match snapshot 1`] = `
value=""
/>
</View>
<View
style={
Array [
undefined,
null,
]
}
>
<Text
accessibilityRole="button"
onPress={[Function]}
style={
Object {
"color": "#3d3c40",
"fontWeight": "bold",
"marginBottom": 10,
"marginLeft": 10,
"marginTop": 20,
}
}
>
<Icon
allowFontScaling={false}
name="caret-down"
size={12}
/>
</Text>
</View>
<CustomList
data={
Array [

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {isLandscape} from 'app/selectors/device';
import {General} from 'mattermost-redux/constants';
import {getChannels, joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import {joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@ -15,11 +15,13 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel';
import {handleSelectChannel, setChannelDisplayName, loadPublicAndArchivedChannels} from 'app/actions/views/channel';
import MoreChannels from './more_channels';
const joinableChannels = createSelector(
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
const joinablePublicChannels = createSelector(
getChannelsInCurrentTeam,
getMyChannelMemberships,
(channels, myMembers) => {
@ -29,11 +31,19 @@ const joinableChannels = createSelector(
}
);
const teamArchivedChannels = createSelector(
getChannelsInCurrentTeam,
(channels) => {
return channels.filter((c) => c.delete_at !== 0);
}
);
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const roles = getCurrentUserRoles(state);
const channels = joinableChannels(state);
const channels = joinablePublicChannels(state);
const archivedChannels = teamArchivedChannels(state);
const currentTeamId = getCurrentTeamId(state);
return {
@ -41,8 +51,10 @@ function mapStateToProps(state) {
currentUserId: getCurrentUserId(state),
currentTeamId,
channels,
archivedChannels,
theme: getTheme(state),
isLandscape: isLandscape(state),
canShowArchivedChannels: isMinimumServerVersion(state.entities.general.serverVersion, 5, 18),
};
}
@ -51,7 +63,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
handleSelectChannel,
joinChannel,
getChannels,
loadPublicAndArchivedChannels,
searchChannels,
setChannelDisplayName,
}, dispatch),

View file

@ -4,7 +4,8 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {Platform, View} from 'react-native';
import {Platform, View, Text} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {Navigation} from 'react-native-navigation';
import {debounce} from 'mattermost-redux/actions/helpers';
@ -12,6 +13,7 @@ import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import BottomSheet from 'app/utils/bottom_sheet';
import CustomList from 'app/components/custom_list';
import ChannelListRow from 'app/components/custom_list/channel_list_row';
import FormattedText from 'app/components/formatted_text';
@ -33,18 +35,20 @@ export default class MoreChannels extends PureComponent {
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
getChannels: PropTypes.func.isRequired,
loadPublicAndArchivedChannels: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
canCreateChannels: PropTypes.bool.isRequired,
channels: PropTypes.array,
archivedChannels: PropTypes.array,
closeButton: PropTypes.object,
currentUserId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
canShowArchivedChannels: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -59,12 +63,15 @@ export default class MoreChannels extends PureComponent {
super(props, context);
this.searchTimeoutId = 0;
this.page = -1;
this.publicPage = -1;
this.archivedPage = -1;
this.next = true;
this.mounted = false;
this.state = {
channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE),
archivedChannels: props.archivedChannels.slice(0, General.CHANNELS_CHUNK_SIZE),
typeOfChannels: 'public',
loading: false,
adding: false,
term: '',
@ -130,10 +137,11 @@ export default class MoreChannels extends PureComponent {
}
cancelSearch = () => {
const {channels} = this.props;
const {channels, archivedChannels} = this.props;
this.setState({
channels,
archivedChannels,
term: '',
});
};
@ -143,15 +151,17 @@ export default class MoreChannels extends PureComponent {
};
doGetChannels = () => {
const {actions, currentTeamId} = this.props;
const {actions, currentTeamId, canShowArchivedChannels} = this.props;
const {loading, term} = this.state;
if (this.next && !loading && !term && this.mounted) {
this.setState({loading: true}, () => {
actions.getChannels(
actions.loadPublicAndArchivedChannels(
currentTeamId,
this.page + 1,
General.CHANNELS_CHUNK_SIZE
this.publicPage + 1,
this.archivedPage + 1,
General.CHANNELS_CHUNK_SIZE,
canShowArchivedChannels,
).then(this.loadedChannels);
});
}
@ -295,39 +305,80 @@ export default class MoreChannels extends PureComponent {
renderItem = (props) => {
return (
<ChannelListRow {...props}/>
<ChannelListRow
{...props}
isArchived={this.state.typeOfChannels === 'archived'}
/>
);
}
searchChannels = (text) => {
const {actions, channels, currentTeamId} = this.props;
const {actions, channels, archivedChannels, currentTeamId, canShowArchivedChannels} = this.props;
const {typeOfChannels} = this.state;
if (text) {
const filtered = this.filterChannels(channels, text);
this.setState({
channels: filtered,
term: text,
});
clearTimeout(this.searchTimeoutId);
if (typeOfChannels === 'public') {
const filtered = this.filterChannels(channels, text);
this.setState({
channels: filtered,
term: text,
});
clearTimeout(this.searchTimeoutId);
} else if (typeOfChannels === 'archived' && canShowArchivedChannels) {
const filtered = this.filterChannels(archivedChannels, text);
this.setState({
archivedChannels: filtered,
term: text,
});
clearTimeout(this.searchTimeoutId);
}
this.searchTimeoutId = setTimeout(() => {
actions.searchChannels(currentTeamId, text.toLowerCase());
actions.searchChannels(currentTeamId, text.toLowerCase(), typeOfChannels === 'archived');
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
this.cancelSearch();
}
};
handleDropdownClick = () => {
const {formatMessage} = this.context.intl;
const publicChannelsText = formatMessage({id: 'more_channels.publicChannels', defaultMessage: 'Public Channels'});
const archivedChannelsText = formatMessage({id: 'more_channels.archivedChannels', defaultMessage: 'Archived Channels'});
const titleText = formatMessage({id: 'more_channels.dropdownTitle', defaultMessage: 'Show'});
const cancelText = 'Cancel';
BottomSheet.showBottomSheetWithOptions({
options: [publicChannelsText, archivedChannelsText, cancelText],
cancelButtonIndex: 2,
title: titleText,
}, (value) => {
let typeOfChannels;
switch (value) {
case 0:
typeOfChannels = 'public';
break;
case 1:
typeOfChannels = 'archived';
break;
default:
typeOfChannels = this.state.typeOfChannels;
}
this.setState({typeOfChannels});
});
}
render() {
const {formatMessage} = this.context.intl;
const {theme, isLandscape} = this.props;
const {adding, channels, loading, term} = this.state;
const {theme, isLandscape, canShowArchivedChannels} = this.props;
const {adding, channels, archivedChannels, loading, term, typeOfChannels} = this.state;
const more = term ? () => true : this.getChannels;
const style = getStyleFromTheme(theme);
const publicChannelsText = formatMessage({id: 'more_channels.showPublicChannels', defaultMessage: 'Show: Public Channels'});
const archivedChannelsText = formatMessage({id: 'more_channels.showArchivedChannels', defaultMessage: 'Show: Archived Channels'});
let content;
if (adding) {
content = (<Loading/>);
content = (<Loading color={theme.centerChannelColor}/>);
} else {
const searchBarInput = {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
@ -340,6 +391,31 @@ export default class MoreChannels extends PureComponent {
}),
};
let activeChannels = channels;
if (canShowArchivedChannels && typeOfChannels === 'archived') {
activeChannels = archivedChannels;
}
let channelDropdown;
if (canShowArchivedChannels) {
channelDropdown = (
<View style={[style.titleContainer, padding(isLandscape)]}>
<Text
accessibilityRole={'button'}
style={style.channelDropdown}
onPress={this.handleDropdownClick}
>
{typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText}
{' '}
<Icon
name={'caret-down'}
/>
</Text>
</View>
);
}
content = (
<React.Fragment>
<View style={[style.searchBar, padding(isLandscape)]}>
@ -362,8 +438,9 @@ export default class MoreChannels extends PureComponent {
value={term}
/>
</View>
{channelDropdown}
<CustomList
data={channels}
data={activeChannels}
extraData={loading}
key='custom_list'
loading={loading}
@ -411,5 +488,12 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
channelDropdown: {
color: theme.centerChannelColor,
fontWeight: 'bold',
marginLeft: 10,
marginTop: 20,
marginBottom: 10,
},
};
});

View file

@ -16,7 +16,7 @@ describe('MoreChannels', () => {
const actions = {
handleSelectChannel: jest.fn(),
joinChannel: jest.fn(),
getChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}),
loadPublicAndArchivedChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}),
searchChannels: jest.fn(),
setChannelDisplayName: jest.fn(),
};
@ -25,12 +25,14 @@ describe('MoreChannels', () => {
actions,
canCreateChannels: true,
channels: [{id: 'id', name: 'name', display_name: 'display_name'}],
archivedChannels: [{id: 'id2', name: 'archived', display_name: 'archived channel'}],
closeButton: {},
currentUserId: 'current_user_id',
currentTeamId: 'current_team_id',
theme: Preferences.THEMES.default,
componentId: 'component-id',
isLandscape: false,
canShowArchivedChannels: true,
};
test('should match snapshot', () => {
@ -91,4 +93,20 @@ describe('MoreChannels', () => {
expect(wrapper.state('term')).toEqual('');
expect(wrapper.state('channels')).toEqual(baseProps.channels);
});
test('should search correct channels', () => {
const wrapper = shallow(
<MoreChannels {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
const instance = wrapper.instance();
wrapper.setState({typeOfChannels: 'public'});
instance.searchChannels('display_name');
expect(wrapper.state('channels')).toEqual(baseProps.channels);
wrapper.setState({typeOfChannels: 'archived'});
instance.searchChannels('archived channel');
expect(wrapper.state('archivedChannels')).toEqual(baseProps.archivedChannels);
});
});

View file

@ -420,7 +420,7 @@ export default class MoreDirectMessages extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}

View file

@ -124,7 +124,7 @@ exports[`Permalink should match snapshot 1`] = `
}
>
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>

View file

@ -370,7 +370,7 @@ export default class Permalink extends PureComponent {
</View>
);
} else if (loading) {
postList = <Loading/>;
postList = <Loading color={theme.centerChannelColor}/>;
} else {
postList = (
<PostList

View file

@ -333,11 +333,13 @@ export default class Search extends PureComponent {
});
renderFooter = () => {
if (this.props.isSearchGettingMore) {
const style = getStyleFromTheme(this.props.theme);
const {isSearchGettingMore, theme} = this.props;
if (isSearchGettingMore) {
const style = getStyleFromTheme(theme);
return (
<View style={style.loadingMore}>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
@ -634,7 +636,7 @@ export default class Search extends PureComponent {
id: SEARCHING,
component: (
<View style={style.searching}>
<Loading/>
<Loading color={theme.centerChannelColor}/>
</View>
),
}];

View file

@ -97,11 +97,19 @@ exports[`SelectTeam should match snapshot for teams 1`] = `
listType="flat"
loading={false}
loadingComponent={
<Loading
color="grey"
size="large"
style={Object {}}
/>
<View
style={
Object {
"marginVertical": 10,
}
}
>
<Loading
color="#3d3c40"
size="small"
style={Object {}}
/>
</View>
}
onLoadMore={[Function]}
onRefresh={[Function]}

View file

@ -217,7 +217,7 @@ export default class SelectTeam extends PureComponent {
const style = getStyleFromTheme(theme);
if (this.state.joining) {
return <Loading/>;
return <Loading color={theme.centerChannelColor}/>;
}
if (this.props.teamsRequest.status === RequestStatus.FAILURE) {
@ -262,7 +262,14 @@ export default class SelectTeam extends PureComponent {
<CustomList
data={teams}
loading={this.state.loading}
loadingComponent={<Loading/>}
loadingComponent={
<View style={style.footer}>
<Loading
color={theme.centerChannelColor}
size='small'
/>
</View>
}
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
onLoadMore={this.getTeams}
@ -300,6 +307,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
width: '100%',
height: 1,
},
footer: {
marginVertical: 10,
},
teamWrapper: {
marginTop: 20,
},

View file

@ -272,7 +272,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
exports[`TermsOfService should match snapshot 1`] = `
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>
@ -329,7 +329,7 @@ exports[`TermsOfService should match snapshot for fail of get terms 1`] = `
exports[`TermsOfService should match snapshot for get terms 1`] = `
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>

View file

@ -231,7 +231,7 @@ export default class TermsOfService extends PureComponent {
const textStyles = getMarkdownTextStyles(theme);
if (this.state.loading) {
return <Loading/>;
return <Loading color={theme.centerChannelColor}/>;
}
if (this.state.getTermsError) {

View file

@ -32,7 +32,7 @@ exports[`thread should match snapshot, has root post 1`] = `
}
renderFooter={
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>
@ -87,7 +87,7 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
/>
<Connect(StatusBar) />
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>
@ -112,7 +112,7 @@ exports[`thread should match snapshot, render footer 1`] = `
}
renderFooter={
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>
@ -157,7 +157,7 @@ exports[`thread should match snapshot, render footer 3`] = `
/>
<Connect(StatusBar) />
<Loading
color="grey"
color="#3d3c40"
size="large"
style={Object {}}
/>

View file

@ -53,7 +53,7 @@ export default class ThreadAndroid extends ThreadBase {
);
} else {
content = (
<Loading/>
<Loading color={theme.centerChannelColor}/>
);
}

View file

@ -85,7 +85,7 @@ export default class ThreadIOS extends ThreadBase {
);
} else {
content = (
<Loading/>
<Loading color={theme.centerChannelColor}/>
);
}

View file

@ -106,13 +106,15 @@ export default class ThreadBase extends PureComponent {
};
renderFooter = () => {
if (!this.hasRootPost() && this.props.threadLoadingStatus.status !== RequestStatus.STARTED) {
const {theme, threadLoadingStatus} = this.props;
if (!this.hasRootPost() && threadLoadingStatus.status !== RequestStatus.STARTED) {
return (
<DeletedPost theme={this.props.theme}/>
<DeletedPost theme={theme}/>
);
} else if (this.props.threadLoadingStatus.status === RequestStatus.STARTED) {
} else if (threadLoadingStatus.status === RequestStatus.STARTED) {
return (
<Loading/>
<Loading color={theme.centerChannelColor}/>
);
}

View file

@ -48,11 +48,11 @@ export const getMatchTermForChannelMention = (() => {
lastIsSearch = isSearch;
if (match) {
if (isSearch) {
lastMatchTerm = match[1];
lastMatchTerm = match[1].toLowerCase();
} else if (match.index > 0 && value[match.index - 1] === '~') {
lastMatchTerm = null;
} else {
lastMatchTerm = match[2];
lastMatchTerm = match[2].toLowerCase();
}
} else {
lastMatchTerm = null;
@ -152,7 +152,7 @@ export const filterMyChannels = createSelector(
if (matchTerm) {
channels = myChannels.filter((c) => {
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL) &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
(c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().startsWith(matchTerm));
});
} else {
channels = myChannels.filter((c) => {
@ -175,7 +175,7 @@ export const filterOtherChannels = createSelector(
let channels;
if (matchTerm) {
channels = otherChannels.filter((c) => {
return (c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
return (c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().startsWith(matchTerm));
});
} else {
channels = otherChannels;
@ -200,9 +200,9 @@ export const filterPublicChannels = createSelector(
if (matchTerm) {
channels = myChannels.filter((c) => {
return c.type === General.OPEN_CHANNEL &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
(c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().startsWith(matchTerm));
}).concat(
otherChannels.filter((c) => c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm))
otherChannels.filter((c) => c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().startsWith(matchTerm))
);
} else {
channels = myChannels.filter((c) => {
@ -232,7 +232,7 @@ export const filterPrivateChannels = createSelector(
if (matchTerm) {
channels = myChannels.filter((c) => {
return c.type === General.PRIVATE_CHANNEL &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
(c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().startsWith(matchTerm));
});
} else {
channels = myChannels.filter((c) => {
@ -261,10 +261,10 @@ export const filterDirectAndGroupMessages = createSelector(
let channels;
if (matchTerm) {
channels = myChannels.filter((c) => {
if (c.type === General.DM_CHANNEL && (originalChannels[c.id].display_name.startsWith(matchTerm))) {
if (c.type === General.DM_CHANNEL && (originalChannels[c.id].display_name.toLowerCase().startsWith(matchTerm))) {
return true;
}
if (c.type === General.GM_CHANNEL && (c.name.startsWith(matchTerm) || c.display_name.replace(/ /g, '').startsWith(matchTerm))) {
if (c.type === General.GM_CHANNEL && (c.name.toLowerCase().startsWith(matchTerm) || c.display_name.toLowerCase().replace(/ /g, '').startsWith(matchTerm))) {
return true;
}
return false;

View file

@ -110,5 +110,6 @@ export const GlobalStyles = StyleSheet.create({
paddingLeft: 10,
alignSelf: 'stretch',
borderRadius: 3,
color: '#3d3c40',
},
});

View file

@ -82,7 +82,7 @@ export default class ImageCacheManager {
notifyAll(uri, pathWithPrefix);
} catch (e) {
RNFetchBlob.fs.unlink(pathWithPrefix);
notifyAll(uri, uri);
notifyAll(uri, null);
unsubscribe(uri);
return null;
}

View file

@ -493,7 +493,12 @@
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?",
"more_channels.archivedChannels": "Archived Channels",
"more_channels.dropdownTitle": "Show",
"more_channels.noMore": "No more channels to join",
"more_channels.publicChannels": "Public Channels",
"more_channels.showArchivedChannels": "Show: Archived Channels",
"more_channels.showPublicChannels": "Show: Public Channels",
"more_channels.title": "More Channels",
"msg_typing.areTyping": "{users} and {last} are typing...",
"msg_typing.isTyping": "{user} is typing...",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/base/images/icons/excel.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,006 B

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/base/images/icons/patch.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/base/images/icons/ppt.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/base/images/icons/word.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -5,7 +5,6 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* MattermostTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* MattermostTests.m */; };
0111A42B7F264BCF8CBDE3ED /* OpenSans-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */; };
@ -266,6 +265,7 @@
D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Bold.ttf"; path = "../assets/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.debug.xcconfig"; sourceTree = "<group>"; };
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = "<group>"; };
81061F4CBB31484A94D5A8EE /* libz.tbd */ = {isa = PBXFileReference; name = "libz.tbd"; path = "usr/lib/libz.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -815,6 +815,58 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
1D22DC6FC0A37F7F657B8274 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Mattermost/Pods-Mattermost-resources.sh",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_ROOT}/../../node_modules/react-native-youtube/assets/YTPlayerView-iframe-player.html",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/YTPlayerView-iframe-player.html",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Mattermost/Pods-Mattermost-resources.sh\"\n";
showEnvVarsInLog = 0;
};
AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;

View file

@ -25,6 +25,15 @@ PODS:
- React
- KeyboardTrackingView (5.5.0):
- React
- libwebp (1.0.3):
- libwebp/demux (= 1.0.3)
- libwebp/mux (= 1.0.3)
- libwebp/webp (= 1.0.3)
- libwebp/demux (1.0.3):
- libwebp/webp
- libwebp/mux (1.0.3):
- libwebp/demux
- libwebp/webp (1.0.3)
- RCTRequired (0.61.2)
- RCTTypeSafety (0.61.2):
- FBLazyVector (= 0.61.2)
@ -191,6 +200,8 @@ PODS:
- React-cxxreact (= 0.61.2)
- React-jsi (= 0.61.2)
- React-jsinspector (0.61.2)
- react-native-cameraroll (1.3.0):
- React
- react-native-cookies (3.2.0):
- React
- react-native-document-picker (3.2.4):
@ -257,6 +268,10 @@ PODS:
- React
- RNDeviceInfo (2.1.2):
- React
- RNFastImage (7.0.2):
- React
- SDWebImage (~> 5.0)
- SDWebImageWebPCoder (~> 0.2.3)
- RNGestureHandler (1.4.1):
- React
- RNKeychain (4.0.1):
@ -272,6 +287,12 @@ PODS:
- React
- RNVectorIcons (6.6.0):
- React
- SDWebImage (5.2.2):
- SDWebImage/Core (= 5.2.2)
- SDWebImage/Core (5.2.2)
- SDWebImageWebPCoder (0.2.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.0)
- Sentry (4.4.0):
- Sentry/Core (= 4.4.0)
- Sentry/Core (4.4.0)
@ -301,6 +322,7 @@ DEPENDENCIES:
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
- react-native-cookies (from `../node_modules/react-native-cookies/ios`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
@ -326,6 +348,7 @@ DEPENDENCIES:
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNFastImage (from `../node_modules/react-native-fast-image`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNReactNativeDocViewer (from `../node_modules/react-native-doc-viewer`)
@ -340,6 +363,9 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- libwebp
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- Swime
- XCDYouTubeKit
@ -382,6 +408,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
react-native-cameraroll:
:path: "../node_modules/@react-native-community/cameraroll"
react-native-cookies:
:path: "../node_modules/react-native-cookies/ios"
react-native-document-picker:
@ -430,6 +458,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/async-storage"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNKeychain:
@ -457,6 +487,7 @@ SPEC CHECKSUMS:
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
jail-monkey: 0830c18bb6f085938a4c8529d554f9b29230a5a4
KeyboardTrackingView: d4d7236123b401ed9b1e02869e7943117740c522
libwebp: 057912d6d0abfb6357d8bb05c0ea470301f5d61e
RCTRequired: c639d59ed389cfb1f1203f65c2ea946d8ec586e2
RCTTypeSafety: dc23fb655d6c77667c78e327bf661bc11e3b8aec
RCTYouTube: a36b9960e040063877fb8dc61fff19d3bd1a97ff
@ -467,6 +498,7 @@ SPEC CHECKSUMS:
React-jsi: 32285a21b1b24c36060493ed3057a34677d58d09
React-jsiexecutor: 8909917ff7d8f21a57e443a866fd8d4560e50c65
React-jsinspector: 111d7d342b07a904c400592e02a2b958f1098b60
react-native-cameraroll: ad20f5a93c25cb83a76455df57a2c62fbb63aaed
react-native-cookies: 854d59c4135c70b92a02ca4930e68e4e2eb58150
react-native-document-picker: c36bf5f067a581657ecaf7124dcd921a8be19061
react-native-image-picker: fd93361c666f397bdf72f9c6c23f13d2685b9173
@ -491,6 +523,7 @@ SPEC CHECKSUMS:
rn-fetch-blob: 3258c6483235bb7daf16cf921306ffb18406071f
RNCAsyncStorage: 5ae4d57458804e99f73d427214442a6b10a53856
RNDeviceInfo: fd8296de6fca8b743cdc499b896f48e8a9f1faf5
RNFastImage: 9b0c22643872bb7494c8d87bbbb66cc4c0d9e7a2
RNGestureHandler: 4cb47a93019c1a201df2644413a0a1569a51c8aa
RNKeychain: 45dbd50d1ac4bd42c3740f76ffb135abf05746d0
RNReactNativeDocViewer: 571c6ac38483531b8fb521d02a6ba54652ed10a7
@ -498,6 +531,8 @@ SPEC CHECKSUMS:
RNSentry: 2803ba8c8129dcf26b79e9b4d8c80168be6e4390
RNSVG: be27aa7c58819f97399388ae53d7fa0572f32c7f
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
SDWebImage: 5fcdb02cc35e05fc35791ec514b191d27189f872
SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987
Sentry: 26650184fe71eb7476dfd2737acb5ea6cc64b4b1
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
XCDYouTubeKit: 4ca4e3322fa556ec1c32932d378365c15ce1386e

1214
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"dependencies": {
"@babel/runtime": "7.6.3",
"@react-native-community/async-storage": "1.6.2",
"@react-native-community/cameraroll": "1.3.0",
"@react-native-community/netinfo": "4.4.0",
"@sentry/react-native": "1.0.9",
"analytics-react-native": "1.2.0",
@ -23,7 +24,7 @@
"intl": "1.2.5",
"jail-monkey": "2.3.0",
"jsc-android": "241213.1.0",
"mattermost-redux": "github:mattermost/mattermost-redux#4886198f19da9b15c7f24bdd2d7835b816b52534",
"mattermost-redux": "github:mattermost/mattermost-redux#e3d38bf92c5a8b5066c0a6252fb750ccd7a0bf98",
"mime-db": "1.42.0",
"moment-timezone": "0.5.27",
"prop-types": "15.7.2",
@ -41,6 +42,7 @@
"react-native-document-picker": "3.2.4",
"react-native-exception-handler": "2.10.8",
"react-native-gesture-handler": "1.4.1",
"react-native-fast-image": "7.0.2",
"react-native-haptic-feedback": "1.8.2",
"react-native-image-gallery": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
"react-native-image-picker": "0.28.1",
@ -114,7 +116,8 @@
"redux-mock-store": "1.5.3",
"remote-redux-devtools": "0.5.16",
"socketcluster": "14.4.1",
"underscore": "1.9.1"
"underscore": "1.9.1",
"util": "0.12.1"
},
"scripts": {
"start": "node ./node_modules/react-native/local-cli/cli.js start",
@ -148,7 +151,7 @@
"assets/images/video_player/(.*).png": "<rootDir>/dist/assets/images/video_player/$1@2x.png"
},
"transformIgnorePatterns": [
"node_modules/(?!react-native|jail-monkey|@sentry/react-native|react-navigation)"
"node_modules/(?!react-native|jail-monkey|@sentry/react-native|react-navigation|@react-native-community/cameraroll)"
]
}
}