Merge branch 'master' into mark-as-unread

This commit is contained in:
Harrison Healey 2019-10-16 11:21:02 -04:00
commit 4899c2f769
26 changed files with 2741 additions and 2505 deletions

View file

@ -123,7 +123,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 238
versionCode 239
versionName "1.24.0"
multiDexEnabled = true
ndk {

View file

@ -4,9 +4,9 @@
import {addChannelMember} from 'mattermost-redux/actions/channels';
export function handleAddChannelMembers(channelId, members) {
return async (dispatch, getState) => {
return async (dispatch) => {
try {
const requests = members.map((m) => dispatch(addChannelMember(channelId, m, getState)));
const requests = members.map((m) => dispatch(addChannelMember(channelId, m)));
return await Promise.all(requests);
} catch (error) {

View file

@ -41,6 +41,7 @@ function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
const post = ownProps.post || getPost(state, ownProps.postId);
const previousPost = getPost(state, ownProps.previousPostId);
const beforePrevPost = getPost(state, ownProps.beforePrevPostId);
const myPreferences = getMyPreferences(state);
const currentUserId = getCurrentUserId(state);
@ -84,6 +85,8 @@ function makeMapStateToProps() {
isFlagged: isPostFlagged(post.id, myPreferences),
isCommentMention,
isLandscape: isLandscape(state),
previousPostExists: Boolean(previousPost),
beforePrevPostUserId: (beforePrevPost ? beforePrevPost.user_id : null),
};
};
}

View file

@ -68,6 +68,8 @@ export default class Post extends PureComponent {
location: PropTypes.string,
isBot: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
previousPostExists: PropTypes.bool,
beforePrevPostUserId: PropTypes.string,
};
static defaultProps = {
@ -250,6 +252,8 @@ export default class Post extends PureComponent {
skipPinnedHeader,
location,
isLandscape,
previousPostExists,
beforePrevPostUserId,
} = this.props;
if (!post) {
@ -299,6 +303,8 @@ export default class Post extends PureComponent {
onUsernamePress={onUsernamePress}
renderReplies={renderReplies}
theme={theme}
previousPostExists={previousPostExists}
beforePrevPostUserId={beforePrevPostUserId}
/>
);
}

View file

@ -43,6 +43,9 @@ export default class PostHeader extends PureComponent {
isGuest: PropTypes.bool,
userTimezone: PropTypes.string,
enableTimezone: PropTypes.bool,
previousPostExists: PropTypes.bool,
post: PropTypes.object,
beforePrevPostUserId: PropTypes.string,
};
static defaultProps = {
@ -58,11 +61,18 @@ export default class PostHeader extends PureComponent {
};
renderCommentedOnMessage = () => {
if (!this.props.renderReplies || !this.props.commentedOnDisplayName) {
const {
beforePrevPostUserId,
commentedOnDisplayName,
post,
previousPostExists,
renderReplies,
theme,
} = this.props;
if (!renderReplies || !commentedOnDisplayName || (!previousPostExists && post.user_id === beforePrevPostUserId)) {
return null;
}
const {commentedOnDisplayName, theme} = this.props;
const style = getStyleSheet(theme);
const displayName = commentedOnDisplayName;

View file

@ -260,6 +260,7 @@ export default class PostList extends PureComponent {
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
// comes after this one in the list
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
const beforePrevPostId = index < this.props.postIds.length - 2 ? this.props.postIds[index + 2] : null;
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
const postProps = {
@ -274,6 +275,7 @@ export default class PostList extends PureComponent {
onPress: this.props.onPostPress,
renderReplies: this.props.renderReplies,
shouldRenderReplyButton: this.props.shouldRenderReplyButton,
beforePrevPostId,
};
if (PostListUtils.isCombinedUserActivityPost(item)) {

View file

@ -87,6 +87,7 @@ exports[`ChannelItem should match snapshot 1`] = `
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -202,6 +203,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -316,6 +318,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -430,6 +433,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -533,6 +537,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -637,6 +642,7 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -745,6 +751,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
@ -851,6 +858,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {

View file

@ -245,6 +245,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 16,
lineHeight: 24,
paddingRight: 10,
maxWidth: '80%',
flex: 1,
alignSelf: 'center',
fontFamily: 'Open Sans',

View file

@ -27,6 +27,7 @@ exports[`channel_info should match snapshot 1`] = `
isArchived={false}
isBot={false}
isGroupConstrained={false}
isLandscape={false}
isTeammateGuest={false}
memberCount={2}
onPermalinkPress={[Function]}

View file

@ -603,6 +603,7 @@ export default class ChannelInfo extends PureComponent {
isTeammateGuest={isTeammateGuest}
hasGuests={currentChannelGuestCount > 0}
isGroupConstrained={currentChannel.group_constrained}
isLandscape={isLandscape}
/>
}
<View style={style.rowsContainer}>

View file

@ -9,7 +9,6 @@ import {
Text,
TouchableHighlight,
View,
SafeAreaView,
} from 'react-native';
import {intlShape} from 'react-intl';
@ -19,6 +18,7 @@ import ChannelIcon from 'app/components/channel_icon';
import FormattedDate from 'app/components/formatted_date';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
@ -44,6 +44,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
hasGuests: PropTypes.bool.isRequired,
isGroupConstrained: PropTypes.bool,
timeZone: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
};
static contextTypes = {
@ -142,6 +143,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
isBot,
isGroupConstrained,
timeZone,
isLandscape,
} = this.props;
const style = getStyleSheet(theme);
@ -152,101 +154,99 @@ export default class ChannelInfoHeader extends React.PureComponent {
style.detail;
return (
<SafeAreaView>
<View style={style.container}>
<View style={[style.channelNameContainer, style.row]}>
<ChannelIcon
isInfo={true}
membersCount={memberCount - 1}
size={16}
status={status}
theme={theme}
type={type}
isArchived={isArchived}
isBot={isBot}
/>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.channelName}
>
{displayName}
</Text>
</View>
{this.renderHasGuestText(style)}
{purpose.length > 0 &&
<View style={style.section}>
<TouchableHighlight
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
onLongPress={this.handlePurposeLongPress}
>
<View style={style.row}>
<FormattedText
style={style.header}
id='channel_info.purpose'
defaultMessage='Purpose'
/>
<Markdown
onPermalinkPress={onPermalinkPress}
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={purpose}
/>
</View>
</TouchableHighlight>
</View>
}
{header.length > 0 &&
<View style={style.section}>
<TouchableHighlight
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
onLongPress={this.handleHeaderLongPress}
>
<View style={style.row}>
<FormattedText
style={style.header}
id='channel_info.header'
defaultMessage='Header'
/>
<Markdown
onPermalinkPress={onPermalinkPress}
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={header}
onChannelLinkPress={popToRoot}
/>
</View>
</TouchableHighlight>
</View>
}
{isGroupConstrained &&
<Text style={[style.createdBy, style.row]}>
<FormattedText
id='mobile.routes.channelInfo.groupManaged'
defaultMessage='Members are managed by linked groups'
/>
</Text>
}
{creator &&
<Text style={[style.createdBy, style.row]}>
<FormattedText
id='mobile.routes.channelInfo.createdBy'
defaultMessage='Created by {creator} on '
values={{
creator,
}}
/>
<FormattedDate
format='LL'
timeZone={timeZone}
value={createAt}
/>
</Text>
}
<View style={style.container}>
<View style={[style.channelNameContainer, style.row, padding(isLandscape)]}>
<ChannelIcon
isInfo={true}
membersCount={memberCount - 1}
size={16}
status={status}
theme={theme}
type={type}
isArchived={isArchived}
isBot={isBot}
/>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.channelName}
>
{displayName}
</Text>
</View>
</SafeAreaView>
{this.renderHasGuestText(style)}
{purpose.length > 0 &&
<View style={[style.section, padding(isLandscape, -15)]}>
<TouchableHighlight
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
onLongPress={this.handlePurposeLongPress}
>
<View style={style.row}>
<FormattedText
style={style.header}
id='channel_info.purpose'
defaultMessage='Purpose'
/>
<Markdown
onPermalinkPress={onPermalinkPress}
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={purpose}
/>
</View>
</TouchableHighlight>
</View>
}
{header.length > 0 &&
<View style={[style.section, padding(isLandscape, -15)]}>
<TouchableHighlight
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
onLongPress={this.handleHeaderLongPress}
>
<View style={style.row}>
<FormattedText
style={style.header}
id='channel_info.header'
defaultMessage='Header'
/>
<Markdown
onPermalinkPress={onPermalinkPress}
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={header}
onChannelLinkPress={popToRoot}
/>
</View>
</TouchableHighlight>
</View>
}
{isGroupConstrained &&
<Text style={[style.createdBy, style.row, padding(isLandscape)]}>
<FormattedText
id='mobile.routes.channelInfo.groupManaged'
defaultMessage='Members are managed by linked groups'
/>
</Text>
}
{creator &&
<Text style={[style.createdBy, style.row, padding(isLandscape)]}>
<FormattedText
id='mobile.routes.channelInfo.createdBy'
defaultMessage='Created by {creator} on '
values={{
creator,
}}
/>
<FormattedDate
format='LL'
timeZone={timeZone}
value={createAt}
/>
</Text>
}
</View>
);
}
}

View file

@ -43,6 +43,7 @@ describe('channel_info_header', () => {
isTeammateGuest: false,
hasGuests: false,
isGroupConstrained: false,
isLandscape: false,
};
test('should match snapshot', async () => {

View file

@ -87,6 +87,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
footer: {
marginTop: 10,
marginHorizontal: 15,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
},

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import {setStoreFromLocalData} from 'mattermost-redux/actions/general';
@ -15,6 +16,7 @@ function mapStateToProps(state) {
return {
...state.views.selectServer,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

@ -18,6 +18,7 @@ import {Client4} from 'mattermost-redux/client';
import {ViewTypes} from 'app/constants';
import Loading from 'app/components/loading';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import StatusBar from 'app/components/status_bar';
import {resetToChannel} from 'app/actions/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -70,6 +71,7 @@ class SSO extends PureComponent {
handleSuccessfulLogin: PropTypes.func.isRequired,
setStoreFromLocalData: PropTypes.func.isRequired,
}).isRequired,
isLandscape: PropTypes.bool.isRequired,
};
useWebkit = true;
@ -203,7 +205,7 @@ class SSO extends PureComponent {
};
render() {
const {theme} = this.props;
const {theme, isLandscape} = this.props;
const {error, messagingEnabled, renderWebView, jsCode} = this.state;
const style = getStyleSheet(theme);
@ -236,7 +238,7 @@ class SSO extends PureComponent {
}
return (
<View style={style.container}>
<View style={[style.container, padding(isLandscape)]}>
<StatusBar/>
{content}
</View>

View file

@ -52,7 +52,7 @@ export function emptyFunction() { // eslint-disable-line no-empty-function
export function hapticFeedback(method = 'impactLight') {
ReactNativeHapticFeedback.trigger(method, {
enableVibrateFallback: true,
enableVibrateFallback: false,
ignoreAndroidSystemSettings: false,
});
}

View file

@ -454,6 +454,8 @@
"mobile.share_extension.error_message": "An error has occurred while using the share extension.",
"mobile.share_extension.error_title": "Extension Error",
"mobile.share_extension.team": "Team",
"mobile.share_extension.too_long_message": "Character count: {count}/{max}",
"mobile.share_extension.too_long_title": "Message is too long",
"mobile.sidebar_settings.permanent": "Permanent Sidebar",
"mobile.sidebar_settings.permanent_description": "Keep the sidebar open permanently",
"mobile.storage_permission_denied_description": "Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.",

View file

@ -2856,7 +2856,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 239;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@ -2917,7 +2917,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 239;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View file

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

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.24.0</string>
<key>CFBundleVersion</key>
<string>238</string>
<string>239</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

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

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.24.0</string>
<key>CFBundleVersion</key>
<string>238</string>
<string>239</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View file

@ -0,0 +1,30 @@
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
index 28d5870..3f70983 100644
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
@@ -455,12 +455,19 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking
}
if (videoURL) { // Protect against reported crash
- NSError *error = nil;
- [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
- if (error) {
- self.callback(@[@{@"error": error.localizedFailureReason}]);
- return;
- }
+ NSError *error = nil;
+
+ // If we have write access to the source file, move it. Otherwise use copy.
+ if ([fileManager isWritableFileAtPath:[videoURL path]]) {
+ [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
+ } else {
+ [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
+ }
+
+ if (error) {
+ self.callback(@[@{@"error": error.localizedFailureReason}]);
+ return;
+ }
}
}

View file

@ -406,17 +406,34 @@ export default class ExtensionPost extends PureComponent {
onPost = () => {
const {channelId, files, value} = this.state;
const {currentUserId} = this.props;
const {formatMessage} = this.context.intl;
const data = {
channelId,
currentUserId,
files,
token: this.token,
url: this.url,
value,
};
if (value.length > MAX_MESSAGE_LENGTH) {
Alert.alert(
formatMessage({
id: 'mobile.share_extension.too_long_title',
defaultMessage: 'Message is too long',
}),
formatMessage({
id: 'mobile.share_extension.too_long_message',
defaultMessage: 'Character count: {count}/{max}',
}, {
count: value.length,
max: MAX_MESSAGE_LENGTH,
})
);
} else {
const data = {
channelId,
currentUserId,
files,
token: this.token,
url: this.url,
value,
};
this.onClose(data);
this.onClose(data);
}
};
renderBody = () => {
@ -435,7 +452,6 @@ export default class ExtensionPost extends PureComponent {
<TextInput
ref={this.getInputRef}
autoCapitalize='sentences'
maxLength={MAX_MESSAGE_LENGTH}
multiline={true}
onBlur={this.handleBlur}
onChangeText={this.handleTextChange}
@ -566,6 +582,29 @@ export default class ExtensionPost extends PureComponent {
);
};
lengthCounterColor = (count) => {
if (count < 0) {
return styles.textTooLong;
}
return styles.textLengthOk;
}
renderMessageLengthRemaining = () => {
const {value} = this.state;
const messageLengthRemaining = MAX_MESSAGE_LENGTH - value.length;
if (value.length === 0) {
return null;
}
const renderStyle = [styles.messageLengthRemaining, this.lengthCounterColor(messageLengthRemaining)];
return (
<Text style={renderStyle}>
{messageLengthRemaining}
</Text>
);
};
render() {
const {formatMessage} = this.context.intl;
const {maxFileSize} = this.props;
@ -610,6 +649,7 @@ export default class ExtensionPost extends PureComponent {
<View style={styles.wrapper}>
{this.renderBody()}
<View style={styles.flex}>
{this.renderMessageLengthRemaining()}
{this.renderTeamButton()}
{this.renderChannelButton()}
</View>
@ -654,6 +694,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
padding: 15,
},
messageLengthRemaining: {
paddingTop: 5,
paddingBottom: 5,
paddingLeft: 15,
paddingRight: 15,
opacity: 0.5,
},
textLengthOk: {
color: theme.centerChannelColor,
},
textTooLong: {
color: theme.errorTextColor,
},
input: {
flex: 1,
color: theme.centerChannelColor,

View file

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Alert, PermissionsAndroid} from 'react-native';
import {shallowWithIntl} from 'test/intl-test-helper';
import ExtensionPost from './extension_post';
jest.spyOn(Alert, 'alert').mockReturnValue(true);
jest.spyOn(PermissionsAndroid, 'check').mockReturnValue(PermissionsAndroid.RESULTS.GRANTED);
jest.mock('react-navigation-stack/dist/views/TouchableItem', () => {});
jest.mock('NativeModules', () => ({
MattermostShare: {
close: jest.fn(),
},
RNKeychainManager: {
SECURITY_LEVEL_ANY: 'ANY',
},
}));
jest.mock('app/mattermost_managed', () => ({
getConfig: jest.fn().mockReturnValue(false),
}));
const MAX_MESSAGE_LENGTH = 4000;
describe('ExtensionPost', () => {
const baseProps = {
actions: {
getTeamChannels: jest.fn(),
},
channelId: 'channel-id',
channels: {},
currentUserId: 'current-user-id',
maxFileSize: 1024,
navigation: {
setParams: jest.fn(),
},
teamId: 'team-id',
};
const wrapper = shallowWithIntl(
<ExtensionPost {...baseProps}/>
);
const instance = wrapper.instance();
const postMessage = (message) => {
wrapper.setState({value: message});
instance.onPost();
};
test('should show Alert dialog if shared message is longer than maximum length permitted', () => {
const longMessage = 'a'.repeat(MAX_MESSAGE_LENGTH + 1);
postMessage(longMessage);
expect(Alert.alert).toHaveBeenCalledTimes(1);
});
test('should not show Alert dialog if shared message is within maximum length permitted', () => {
const message = 'a'.repeat(MAX_MESSAGE_LENGTH - 1);
postMessage(message);
expect(Alert.alert).not.toHaveBeenCalled();
});
test('should not show Alert dialog if shared message is at maximum length permitted', () => {
const exactLengthMessage = 'a'.repeat(MAX_MESSAGE_LENGTH);
postMessage(exactLengthMessage);
expect(Alert.alert).not.toHaveBeenCalled();
});
});