Merge pull request #2094 from mattermost/release-1.12

Merge Release 1.12 into master
This commit is contained in:
Elias Nahum 2018-09-07 20:06:22 -03:00 committed by GitHub
commit f1274e4977
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 2072 additions and 137 deletions

View file

@ -113,7 +113,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 26
versionCode 136
versionCode 137
versionName "1.12.0"
multiDexEnabled = true
ndk {

View file

@ -7,6 +7,7 @@ import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/c
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {RequestStatus} from 'mattermost-redux/constants';
import {NavigationTypes} from 'app/constants';
@ -56,6 +57,8 @@ export function selectDefaultTeam() {
if (defaultTeam) {
handleTeamChange(defaultTeam.id)(dispatch, getState);
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}

View file

@ -80,7 +80,7 @@ export default class AtMention extends React.PureComponent {
};
getUserDetailsFromMentionName(props) {
let mentionName = props.mentionName;
let mentionName = props.mentionName.toLowerCase();
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {

View file

@ -24,11 +24,13 @@ export default class ChannelMention extends PureComponent {
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
myChannels: PropTypes.array,
myMembers: PropTypes.object,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
deletedPublicChannels: PropTypes.instanceOf(Set),
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
@ -48,7 +50,7 @@ export default class ChannelMention extends PureComponent {
}
componentWillReceiveProps(nextProps) {
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus} = nextProps;
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus, myMembers, deletedPublicChannels} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
@ -74,7 +76,8 @@ export default class ChannelMention extends PureComponent {
if (requestStatus !== RequestStatus.STARTED &&
(myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels)) {
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels ||
myMembers !== this.props.myMembers || deletedPublicChannels !== this.props.deletedPublicChannels)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (isSearch) {
@ -82,7 +85,7 @@ export default class ChannelMention extends PureComponent {
sections.push({
id: 'suggestion.search.public',
defaultMessage: 'Public Channels',
data: publicChannels,
data: publicChannels.filter((cId) => !deletedPublicChannels.has(cId) || myMembers[cId]),
key: 'publicChannels',
});
}

View file

@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@ -12,6 +13,7 @@ import {
filterOtherChannels,
filterPublicChannels,
filterPrivateChannels,
getDeletedPublicChannelsIds,
getMatchTermForChannelMention,
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@ -38,8 +40,10 @@ function mapStateToProps(state, ownProps) {
return {
myChannels,
myMembers: getMyChannelMemberships(state),
otherChannels,
publicChannels,
deletedPublicChannels: getDeletedPublicChannelsIds(state),
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Keyboard, StyleSheet} from 'react-native';
import {Dimensions, Platform, StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import {CalendarList} from 'react-native-calendars';
@ -80,8 +80,6 @@ export default class DateSuggestion extends PureComponent {
const currentDate = (new Date()).toDateString();
const calendarStyle = calendarTheme(theme);
Keyboard.dismiss();
return (
<CalendarList
style={styles.calList}
@ -90,28 +88,55 @@ export default class DateSuggestion extends PureComponent {
futureScrollRange={0}
scrollingEnabled={true}
pagingEnabled={true}
hideArrows={true}
hideArrows={false}
horizontal={true}
showScrollIndicator={true}
onDayPress={this.completeMention}
showWeekNumbers={false}
theme={calendarStyle}
keyboardShouldPersistTaps='always'
/>
);
}
}
const getDateFontSize = () => {
let fontSize = 14;
if (Platform.OS === 'ios') {
const {height, width} = Dimensions.get('window');
if (height < 375 || width < 375) {
fontSize = 13;
}
}
return fontSize;
};
const calendarTheme = memoizeResult((theme) => ({
calendarBackground: theme.centerChannelBg,
monthTextColor: changeOpacity(theme.centerChannelColor, 0.8),
dayTextColor: theme.centerChannelColor,
textSectionTitleColor: changeOpacity(theme.centerChannelColor, 0.25),
'stylesheet.day.basic': {
base: {
width: 22,
height: 22,
alignItems: 'center',
},
text: {
marginTop: 0,
fontSize: getDateFontSize(),
fontWeight: '300',
color: theme.centerChannelColor,
backgroundColor: 'rgba(255, 255, 255, 0)',
lineHeight: 23,
},
today: {
backgroundColor: theme.buttonBg,
width: 33,
height: 33,
borderRadius: 16.5,
width: 24,
height: 24,
borderRadius: 12,
},
todayText: {
color: theme.buttonColor,

View file

@ -124,7 +124,6 @@ export default class SlashSuggestion extends Component {
renderItem = ({item}) => (
<SlashSuggestionItem
displayName={item.display_name}
description={item.auto_complete_desc}
hint={item.auto_complete_hint}
onPress={this.completeSuggestion}

View file

@ -12,7 +12,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
displayName: PropTypes.string,
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
@ -27,7 +26,6 @@ export default class SlashSuggestionItem extends PureComponent {
render() {
const {
displayName,
description,
hint,
theme,
@ -41,7 +39,7 @@ export default class SlashSuggestionItem extends PureComponent {
onPress={this.completeSuggestion}
style={style.row}
>
<Text style={style.suggestionName}>{`/${displayName || trigger} ${hint}`}</Text>
<Text style={style.suggestionName}>{`/${trigger} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
</TouchableOpacity>
);

View file

@ -8,13 +8,11 @@ import {
Text,
View,
} from 'react-native';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import CustomListRow from 'app/components/custom_list/custom_list_row';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
export default class UserListRow extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
@ -58,6 +56,13 @@ export default class UserListRow extends React.PureComponent {
}, {username});
}
if (user.delete_at > 0) {
usernameDisplay = formatMessage({
id: 'more_direct_channels.directchannel.deactivated',
defaultMessage: '{displayname} - Deactivated',
}, {displayname: usernameDisplay});
}
return (
<CustomListRow
id={id}

View file

@ -0,0 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import UserListRow from './user_list_row';
configure({adapter: new Adapter()});
jest.mock('react-intl');
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
jest.mock('rn-fetch-blob', () => ({
fs: {
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
},
}));
jest.mock('rn-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
describe('UserListRow', () => {
const formatMessage = jest.fn();
const baseProps = {
id: '123455',
isMyUser: false,
user: {
id: '21345',
username: 'user',
delete_at: 0,
},
theme: {},
teammateNameDisplay: 'test',
};
test('should match snapshot', () => {
const wrapper = shallow(
<UserListRow {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for deactivated user', () => {
const deactivatedUser = {
id: '21345',
username: 'user',
delete_at: 100,
};
const newProps = {
...baseProps,
user: deactivatedUser,
};
const wrapper = shallow(
<UserListRow {...newProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for currentUser with (you) populated in list', () => {
const newProps = {
...baseProps,
isMyUser: true,
};
const wrapper = shallow(
<UserListRow {...newProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -57,10 +57,10 @@ export default class Emoji extends React.PureComponent {
}
componentWillMount() {
const {displayTextOnly, imageUrl} = this.props;
const {displayTextOnly, emojiName, imageUrl} = this.props;
this.mounted = true;
if (!displayTextOnly && imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
ImageCacheManager.cache(`emoji-${emojiName}`, imageUrl, this.setImageUrl);
}
}
@ -74,7 +74,7 @@ export default class Emoji extends React.PureComponent {
if (!displayTextOnly && imageUrl &&
imageUrl !== this.props.imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
ImageCacheManager.cache(`emoji-${emojiName}`, imageUrl, this.setImageUrl);
}
}

View file

@ -27,34 +27,34 @@ export default class OptionsContext extends PureComponent {
};
}
handleHide = () => {
this.isShowing = false;
this.props.toggleSelected(false);
};
handleHideUnderlay = () => {
if (!this.isShowing) {
this.props.toggleSelected(false, false);
this.props.toggleSelected(false);
}
};
handleShowUnderlay = () => {
this.props.toggleSelected(true, false);
};
handleHide = () => {
this.isShowing = false;
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
};
handleShow = () => {
this.isShowing = this.props.getPostActions().length > 0;
this.props.toggleSelected(true, this.isShowing);
this.show();
this.props.toggleSelected(true);
this.isShowing = this.state.actions.length > 0;
};
hide = () => {
this.setState({
actions: this.props.getPostActions(),
});
if (this.refs.toolTip) {
this.refs.toolTip.hideMenu();
}
this.setState({
actions: this.props.getPostActions(),
});
this.isShowing = false;
this.props.toggleSelected(false);
};
show = (additionalAction) => {
@ -74,7 +74,7 @@ export default class OptionsContext extends PureComponent {
};
handlePress = () => {
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
this.props.toggleSelected(false);
this.props.onPress();
};
@ -87,10 +87,9 @@ export default class OptionsContext extends PureComponent {
actions={this.state.actions}
arrowDirection='down'
longPress={true}
onHide={this.handleHide}
onPress={this.handlePress}
underlayColor='transparent'
onShow={this.handleShow}
onHide={this.handleHide}
>
{this.props.children}
</ToolTip>

View file

@ -110,10 +110,6 @@ export default class Post extends PureComponent {
this.props.actions.insertToDraft(`@${username} `);
};
handleEditDisable = () => {
this.setState({canEdit: false});
};
handlePostDelete = () => {
const {formatMessage} = this.context.intl;
const {actions, currentUserId, post} = this.props;
@ -313,9 +309,7 @@ export default class Post extends PureComponent {
});
toggleSelected = (selected) => {
if (!getToolTipVisible()) {
this.setState({selected});
}
this.setState({selected});
};
handleCopyText = (text) => {

View file

@ -13,7 +13,6 @@ import {
View,
} from 'react-native';
import ProgressiveImage from 'app/components/progressive_image';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {getNearestPoint} from 'app/utils/opengraph';
@ -96,10 +95,22 @@ export default class PostAttachmentOpenGraph extends PureComponent {
});
if (imageUrl) {
ImageCacheManager.cache(null, imageUrl, this.getImageSize);
ImageCacheManager.cache(this.getFilename(imageUrl), imageUrl, this.getImageSize);
}
}
getFilename = (link) => {
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
return `og-${filename}`;
};
getImageSize = (imageUrl) => {
let prefix = '';
if (Platform.OS === 'android') {
@ -141,13 +152,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
originalWidth,
originalHeight,
} = this.state;
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const filename = this.getFilename(link);
const files = [{
caption: filename,
@ -175,6 +180,13 @@ export default class PostAttachmentOpenGraph extends PureComponent {
const style = getStyleSheet(theme);
let description = null;
let source;
if (imageUrl) {
source = {
uri: imageUrl,
};
}
if (openGraphData.description) {
description = (
<View style={style.flex}>
@ -216,19 +228,22 @@ export default class PostAttachmentOpenGraph extends PureComponent {
</View>
{description}
{hasImage &&
<View ref='item'>
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={{width, height}}
>
<ProgressiveImage
ref='image'
style={[style.image, {width, height}]}
imageUri={imageUrl}
resizeMode='contain'
/>
</TouchableWithoutFeedback>
</View>
<View
ref='item'
style={style.imageContainer}
>
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={{width, height}}
>
<Image
ref='image'
style={[style.image, {width, height}]}
source={source}
resizeMode='contain'
/>
</TouchableWithoutFeedback>
</View>
}
</View>
);
@ -266,6 +281,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: changeOpacity(theme.centerChannelColor, 0.7),
marginBottom: 10,
},
imageContainer: {
alignItems: 'center',
},
image: {
borderRadius: 3,
},

View file

@ -25,9 +25,11 @@ export default class QuickTextInput extends React.PureComponent {
* The string value displayed in this input
*/
value: PropTypes.string.isRequired,
refocusInput: PropTypes.bool,
};
static defaultProps = {
refocusInput: true,
delayInputUpdate: false,
editable: true,
value: '',
@ -57,7 +59,7 @@ export default class QuickTextInput extends React.PureComponent {
});
}
this.hadFocus = this.input.isFocused();
this.hadFocus = this.input.isFocused() && this.props.refocusInput;
}
componentDidUpdate(prevProps, prevState) {

View file

@ -30,6 +30,7 @@ export default class Root extends PureComponent {
EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification);
EventEmitter.on(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped);
EventEmitter.on(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams);
EventEmitter.on(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList);
}
}
@ -44,6 +45,7 @@ export default class Root extends PureComponent {
EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification);
EventEmitter.off(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped);
EventEmitter.off(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams);
EventEmitter.off(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList);
}
}
@ -69,11 +71,23 @@ export default class Root extends PureComponent {
setTimeout(this.handleNoTeams, 200);
return;
}
this.navigateToTeamsPage('SelectTeam');
};
errorTeamsList = () => {
if (!this.refs.provider) {
setTimeout(this.errorTeamsList, 200);
return;
}
this.navigateToTeamsPage('ErrorTeamsList');
}
navigateToTeamsPage = (screen) => {
const {currentUrl, navigator, theme} = this.props;
const {intl} = this.refs.provider.getChildContext();
let navigatorButtons;
let passProps = {theme};
if (Platform.OS === 'android') {
navigatorButtons = {
rightButtons: [{
@ -93,8 +107,16 @@ export default class Root extends PureComponent {
};
}
if (screen === 'SelectTeam') {
passProps = {
...passProps,
currentUrl,
userWithoutTeams: true,
};
}
navigator.resetTo({
screen: 'SelectTeam',
screen,
title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}),
animated: false,
backButtonTitle: '',
@ -105,13 +127,9 @@ export default class Root extends PureComponent {
screenBackgroundColor: theme.centerChannelBg,
},
navigatorButtons,
passProps: {
currentUrl,
userWithoutTeams: true,
theme,
},
passProps,
});
};
}
handleNotificationTapped = async () => {
const {navigator} = this.props;

View file

@ -70,6 +70,7 @@ export default class SearchBarAndroid extends PureComponent {
this.state = {
value: props.value,
isFocused: false,
refocusInput: true,
};
}
@ -93,10 +94,12 @@ export default class SearchBarAndroid extends PureComponent {
onSearchButtonPress = () => {
const {value} = this.props;
if (value) {
this.props.onSearchButtonPress(value);
}
this.setState({refocusInput: false}, () => {
if (value) {
this.props.onSearchButtonPress(value);
}
this.setState({refocusInput: true});
});
};
onCancelButtonPress = () => {
@ -215,6 +218,7 @@ export default class SearchBarAndroid extends PureComponent {
<QuickTextInput
ref='input'
blurOnSubmit={blurOnSubmit}
refocusInput={this.state.refocusInput}
value={this.state.value}
autoCapitalize={autoCapitalize}
autoCorrect={false}

View file

@ -100,6 +100,7 @@ ShallowWrapper {
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"marginBottom": 10,
"position": "relative",
"top": 10,
}
@ -291,6 +292,7 @@ ShallowWrapper {
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"marginBottom": 10,
"position": "relative",
"top": 10,
},
@ -510,6 +512,7 @@ ShallowWrapper {
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"marginBottom": 10,
"position": "relative",
"top": 10,
}
@ -701,6 +704,7 @@ ShallowWrapper {
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"marginBottom": 10,
"position": "relative",
"top": 10,
},

View file

@ -101,6 +101,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme, showMore) => {
flexDirection: 'row',
position: 'relative',
top: showMore ? -7.5 : 10,
marginBottom: 10,
},
dividerLeft: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),

View file

@ -8,6 +8,7 @@ const NavigationTypes = keyMirror({
NAVIGATION_CLOSE_MODAL: null,
NAVIGATION_NO_TEAMS: null,
RESTART_APP: null,
NAVIGATION_ERROR_TEAMS: null,
});
export default NavigationTypes;

View file

@ -0,0 +1,166 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorTeamsList should match snapshot 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <ErrorTeamsList
actions={
Object {
"connection": [Function],
"loadMe": [Function],
"logout": [Function],
"selectDefaultTeam": [Function],
}
}
navigator={
Object {
"setOnNavigatorEvent": [Function],
}
}
theme={Object {}}
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Connect(StatusBar) />,
<FailedNetworkAction
errorDescription={
Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
}
}
errorTitle={
Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
}
}
onRetry={[Function]}
theme={Object {}}
/>,
],
"style": Object {
"backgroundColor": undefined,
"flex": 1,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"errorDescription": Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
},
"errorTitle": Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
},
"onRetry": [Function],
"theme": Object {},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Connect(StatusBar) />,
<FailedNetworkAction
errorDescription={
Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
}
}
errorTitle={
Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
}
}
onRetry={[Function]}
theme={Object {}}
/>,
],
"style": Object {
"backgroundColor": undefined,
"flex": 1,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"errorDescription": Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
},
"errorTitle": Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
},
"onRetry": [Function],
"theme": Object {},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;

View file

@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
View,
} from 'react-native';
import FailedNetworkAction from 'app/components/failed_network_action';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
const errorTitle = {
id: 'error.team_not_found.title',
defaultMessage: 'Team Not Found',
};
const errorDescription = {
id: 'mobile.failed_network_action.shortDescription',
defaultMessage: 'Make sure you have an active connection and try again.',
};
export default class ErrorTeamsList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadMe: PropTypes.func.isRequired,
connection: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
selectDefaultTeam: PropTypes.func.isRequired,
}).isRequired,
navigator: PropTypes.object,
theme: PropTypes.object,
};
constructor(props) {
super(props);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.state = {
loading: false,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
}
goToChannelView = () => {
const {navigator, theme} = this.props;
navigator.resetTo({
screen: 'Channel',
animated: false,
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg,
},
});
};
getUserInfo = async () => {
this.setState({loading: true});
this.props.actions.connection(true);
await this.props.actions.loadMe();
this.props.actions.connection(false);
this.setState({loading: false});
this.props.actions.selectDefaultTeam();
this.goToChannelView();
}
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
const {logout} = this.props.actions;
if (event.id === 'logout') {
InteractionManager.runAfterInteractions(logout);
}
}
};
render() {
const {theme} = this.props;
const styles = getStyleSheet(theme);
if (this.state.loading) {
return <Loading/>;
}
return (
<View style={styles.container}>
<StatusBar/>
<FailedNetworkAction
onRetry={this.getUserInfo}
theme={theme}
errorTitle={errorTitle}
errorDescription={errorDescription}
/>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
};
});

View file

@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import FailedNetworkAction from 'app/components/failed_network_action';
import ErrorTeamsList from './error_teams_list';
configure({adapter: new Adapter()});
describe('ErrorTeamsList', () => {
const navigator = {
setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function
};
const loadMe = async () => {
return {
data: {},
};
};
const baseProps = {
actions: {
loadMe: () => {}, // eslint-disable-line no-empty-function
connection: () => {}, // eslint-disable-line no-empty-function
logout: () => {}, // eslint-disable-line no-empty-function
selectDefaultTeam: () => {}, // eslint-disable-line no-empty-function
},
theme: {},
navigator,
};
test('should match snapshot', () => {
const wrapper = shallow(
<ErrorTeamsList {...baseProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('should call for userInfo on retry', async () => {
const connection = jest.fn();
const selectDefaultTeam = jest.fn();
const logout = jest.fn();
const actions = {
loadMe,
logout,
selectDefaultTeam,
connection,
};
const newProps = {
...baseProps,
actions,
};
const wrapper = shallow(
<ErrorTeamsList {...newProps}/>
);
wrapper.find(FailedNetworkAction).props().onRetry();
await loadMe();
expect(selectDefaultTeam).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {logout, loadMe} from 'mattermost-redux/actions/users';
import {connection} from 'app/actions/device';
import {selectDefaultTeam} from 'app/actions/views/select_team';
import ErrorTeamsList from './error_teams_list.js';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
logout,
selectDefaultTeam,
connection,
loadMe,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(ErrorTeamsList);

View file

@ -58,5 +58,6 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider);
Navigation.registerComponent('TimezoneSettings', () => wrapWithContextProvider(require('app/screens/timezone').default), store, Provider);
Navigation.registerComponent('ErrorTeamsList', () => wrapWithContextProvider(require('app/screens/error_teams_list').default), store, Provider);
Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(require('app/screens/user_profile').default), store, Provider);
}

View file

@ -104,7 +104,7 @@ export default class Search extends PureComponent {
}
componentDidUpdate(prevProps) {
const {searchingStatus: status, recent} = this.props;
const {searchingStatus: status, recent, enableDateSuggestion} = this.props;
const {searchingStatus: prevStatus} = prevProps;
const recentLength = recent.length;
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED);
@ -114,12 +114,13 @@ export default class Search extends PureComponent {
}
if (shouldScroll) {
requestAnimationFrame(() => {
setTimeout(() => {
const modifiersCount = enableDateSuggestion ? 5 : 2;
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
animated: true,
offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLength * RECENT_LABEL_HEIGHT) + ((recentLength + 1) * RECENT_SEPARATOR_HEIGHT),
offset: SECTION_HEIGHT + (modifiersCount * MODIFIER_LABEL_HEIGHT) + (recentLength * RECENT_LABEL_HEIGHT) + ((recentLength + 1) * RECENT_SEPARATOR_HEIGHT),
});
});
}, 100);
}
}

View file

@ -242,3 +242,20 @@ export const makeGetMatchTermForDateMention = () => {
return lastMatchTerm;
};
};
export const getDeletedPublicChannelsIds = createSelector(
getMyChannels,
getOtherChannels,
(myChannels, otherChannels) => {
const channels = myChannels.filter((c) => {
return (c.type === General.OPEN_CHANNEL);
}).concat(otherChannels);
return new Set(channels.reduce((acc, c) => {
if (c.delete_at !== 0) {
acc.push(c.id);
}
return acc;
}, []));
}
);

View file

@ -27,33 +27,33 @@ export default class ImageCacheManager {
listener(path);
} else {
addListener(uri, listener);
if (!uri.startsWith('http')) {
if (uri.startsWith('http')) {
try {
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const options = {
session: uri,
timeout: 10000,
indicator: true,
overwrite: true,
path,
certificate,
};
this.downloadTask = await RNFetchBlob.config(options).fetch('GET', uri);
if (this.downloadTask.respInfo.respType === 'text') {
throw new Error();
}
notifyAll(uri, path);
} catch (e) {
RNFetchBlob.fs.unlink(path);
notifyAll(uri, uri);
}
} else {
// In case the uri we are trying to cache is already a local file just notify and return
notifyAll(uri, uri);
return;
}
try {
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const options = {
session: uri,
timeout: 10000,
indicator: true,
overwrite: true,
path,
certificate,
};
this.downloadTask = await RNFetchBlob.config(options).fetch('GET', uri);
if (this.downloadTask.respInfo.respType === 'text') {
throw new Error();
}
notifyAll(uri, path);
} catch (e) {
RNFetchBlob.fs.unlink(path);
notifyAll(uri, uri);
}
unsubscribe(uri);
}
};

View file

@ -6,17 +6,17 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
atomos (0.1.3)
aws-eventstream (1.0.1)
aws-partitions (1.100.0)
aws-sdk-core (3.24.1)
aws-partitions (1.103.0)
aws-sdk-core (3.27.0)
aws-eventstream (~> 1.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-kms (1.7.0)
aws-sdk-core (~> 3)
aws-sdk-kms (1.9.0)
aws-sdk-core (~> 3, >= 3.26.0)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.17.0)
aws-sdk-core (~> 3, >= 3.21.2)
aws-sdk-s3 (1.19.0)
aws-sdk-core (~> 3, >= 3.26.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.3)
@ -41,7 +41,7 @@ GEM
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.3)
fastlane (2.101.1)
fastlane (2.103.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@ -74,22 +74,23 @@ GEM
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.5.7, < 2.0.0)
xcpretty (~> 0.2.8)
xcodeproj (>= 1.6.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-android_change_package_identifier (0.1.0)
fastlane-plugin-android_change_string_app_name (0.1.1)
nokogiri
fastlane-plugin-find_replace_string (0.1.0)
gh_inspector (1.1.3)
google-api-client (0.23.4)
google-api-client (0.23.8)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
googleauth (0.6.4)
signet (~> 0.9)
googleauth (0.6.6)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.12)
@ -125,9 +126,9 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.2.1)
rubyzip (1.2.2)
security (0.1.3)
signet (0.8.1)
signet (0.9.1)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
@ -149,13 +150,13 @@ GEM
unf_ext (0.0.7.5)
unicode-display_width (1.4.0)
word_wrap (1.0.0)
xcodeproj (1.5.9)
xcodeproj (1.6.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.2)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.5)
xcpretty (0.2.8)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)

View file

@ -2427,7 +2427,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 137;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@ -2476,7 +2476,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 137;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View file

@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>136</string>
<string>137</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -23,7 +23,7 @@
<key>CFBundleShortVersionString</key>
<string>1.12.0</string>
<key>CFBundleVersion</key>
<string>136</string>
<string>137</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

@ -19,6 +19,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>136</string>
<string>137</string>
</dict>
</plist>

9
package-lock.json generated
View file

@ -10178,8 +10178,8 @@
}
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#28be3683bff6ae8c563883bb0ff04d7bd9ea31ee",
"from": "github:mattermost/mattermost-redux#28be3683bff6ae8c563883bb0ff04d7bd9ea31ee",
"version": "github:mattermost/mattermost-redux#46dd5d74564fc5aa0d61adeebe73b58b0a36344e",
"from": "github:mattermost/mattermost-redux#46dd5d74564fc5aa0d61adeebe73b58b0a36344e",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "1.2.0",
@ -14712,9 +14712,8 @@
}
},
"react-native-calendars": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/react-native-calendars/-/react-native-calendars-1.20.0.tgz",
"integrity": "sha512-VlRoDcnEAWYE1JBPBh/Bie6baLQCmtuOGhw7V5yk09Y4j7Hy8BtuZIHh2+LU/TFYso+wEHJAFdj6D0QFttDOlg==",
"version": "github:enahum/react-native-calendars#b96954bf85126222b311b638fe458a8194f25bed",
"from": "github:enahum/react-native-calendars#b96954bf85126222b311b638fe458a8194f25bed",
"requires": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",

View file

@ -16,7 +16,7 @@
"intl": "1.2.5",
"jail-monkey": "1.0.0",
"jsc-android": "216113.0.3",
"mattermost-redux": "github:mattermost/mattermost-redux#28be3683bff6ae8c563883bb0ff04d7bd9ea31ee",
"mattermost-redux": "github:mattermost/mattermost-redux#46dd5d74564fc5aa0d61adeebe73b58b0a36344e",
"mime-db": "1.33.0",
"moment-timezone": "0.5.21",
"prop-types": "15.6.1",
@ -26,7 +26,7 @@
"react-native-animatable": "1.2.4",
"react-native-bottom-sheet": "1.0.3",
"react-native-button": "2.3.0",
"react-native-calendars": "1.20.0",
"react-native-calendars": "github:enahum/react-native-calendars#b96954bf85126222b311b638fe458a8194f25bed",
"react-native-circular-progress": "0.2.0",
"react-native-cookies": "3.2.0",
"react-native-device-info": "github:enahum/react-native-device-info#a7bb3cff1086780b2c791a3e43e5b826fdf3ab11",