Merge branch 'master' into mark-as-unread
32
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ export default class EditChannelInfo extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ export default class PostBody extends PureComponent {
|
|||
isFailed={isFailed}
|
||||
onLongPress={this.showPostOptions}
|
||||
postId={post.id}
|
||||
isReplyPost={this.props.isReplyPost}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {}}
|
||||
/>
|
||||
`;
|
||||
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
43
app/components/progressive_image/progressive_image.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
height: ANDROID_TOP_PORTRAIT,
|
||||
},
|
||||
ios: {
|
||||
height: 44,
|
||||
height: 54,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export default class ChannelAddMembers extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ export default class ChannelMembers extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export default class EditPost extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ export default class EditProfile extends PureComponent {
|
|||
return (
|
||||
<View style={[style.container, style.flex]}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ export default class MoreDirectMessages extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ exports[`Permalink should match snapshot 1`] = `
|
|||
}
|
||||
>
|
||||
<Loading
|
||||
color="grey"
|
||||
color="#3d3c40"
|
||||
size="large"
|
||||
style={Object {}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ export default class Permalink extends PureComponent {
|
|||
</View>
|
||||
);
|
||||
} else if (loading) {
|
||||
postList = <Loading/>;
|
||||
postList = <Loading color={theme.centerChannelColor}/>;
|
||||
} else {
|
||||
postList = (
|
||||
<PostList
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
}];
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default class ThreadAndroid extends ThreadBase {
|
|||
);
|
||||
} else {
|
||||
content = (
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default class ThreadIOS extends ThreadBase {
|
|||
);
|
||||
} else {
|
||||
content = (
|
||||
<Loading/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -110,5 +110,6 @@ export const GlobalStyles = StyleSheet.create({
|
|||
paddingLeft: 10,
|
||||
alignSelf: 'stretch',
|
||||
borderRadius: 3,
|
||||
color: '#3d3c40',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
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
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
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
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
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
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
assets/base/images/icons/text.png
Normal file
|
After Width: | Height: | Size: 943 B |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 1.6 KiB |
BIN
assets/base/images/icons/word.png
Normal file → Executable file
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 2.4 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||