diff --git a/android/app/build.gradle b/android/app/build.gradle
index b3b313464..774ca60e6 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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 {
diff --git a/app/actions/views/channel_add_members.js b/app/actions/views/channel_add_members.js
index a560fb797..f09e06616 100644
--- a/app/actions/views/channel_add_members.js
+++ b/app/actions/views/channel_add_members.js
@@ -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) {
diff --git a/app/components/post/index.js b/app/components/post/index.js
index 342fff76a..ae76dd5b6 100644
--- a/app/components/post/index.js
+++ b/app/components/post/index.js
@@ -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),
};
};
}
diff --git a/app/components/post/post.js b/app/components/post/post.js
index a1a555fb5..1afd68ce4 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -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}
/>
);
}
diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js
index fdfa461de..db9b20cdb 100644
--- a/app/components/post_header/post_header.js
+++ b/app/components/post_header/post_header.js
@@ -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;
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index 2ec97afee..87f228c82 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -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)) {
diff --git a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap
index 2987e202c..302538d24 100644
--- a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap
+++ b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap
@@ -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 {
diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js
index bfdf56f88..9d42e742f 100644
--- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js
+++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js
@@ -245,6 +245,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 16,
lineHeight: 24,
paddingRight: 10,
+ maxWidth: '80%',
flex: 1,
alignSelf: 'center',
fontFamily: 'Open Sans',
diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
index 5982e5be3..fd0cd0029 100644
--- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
+++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
@@ -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]}
diff --git a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
index e8f47346b..0122d3937 100644
--- a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
+++ b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
@@ -1,502 +1,545 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`channel_info_header should match snapshot 1`] = `
-
+
-
-
-
- Channel name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ type="O"
+ />
-
-
+ Channel name
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
exports[`channel_info_header should match snapshot when DM and hasGuests and is the teammate 1`] = `
-
+
+
+
+
+ Channel name
+
+
-
-
- Channel name
-
+ />
-
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ >
+
+
+
+
-
+
+
+
+
+
`;
exports[`channel_info_header should match snapshot when DM and hasGuests but its me and not the teammate 1`] = `
-
+
-
-
-
- Channel name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ type="D"
+ />
-
-
+ Channel name
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
-
+
+
+
+
+ Channel name
+
+
-
-
- Channel name
-
+ />
-
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ >
+
+
+
+
-
+
+
+
+
+
`;
exports[`channel_info_header should match snapshot when is group constrained 1`] = `
-
+
-
-
-
- Channel name
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Channel name
-
-`;
-
-exports[`channel_info_header should match snapshot when public channel and hasGuests 1`] = `
-
-
-
-
- Channel name
-
-
-
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`channel_info_header should match snapshot when public channel and hasGuests 1`] = `
+
+
+
+
+ Channel name
+
+
+
+
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js
index 9644e0a7f..5519a65a8 100644
--- a/app/screens/channel_info/channel_info.js
+++ b/app/screens/channel_info/channel_info.js
@@ -603,6 +603,7 @@ export default class ChannelInfo extends PureComponent {
isTeammateGuest={isTeammateGuest}
hasGuests={currentChannelGuestCount > 0}
isGroupConstrained={currentChannel.group_constrained}
+ isLandscape={isLandscape}
/>
}
diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js
index e77f874d2..4e2fc2fab 100644
--- a/app/screens/channel_info/channel_info_header.js
+++ b/app/screens/channel_info/channel_info_header.js
@@ -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 (
-
-
-
-
-
- {displayName}
-
-
- {this.renderHasGuestText(style)}
- {purpose.length > 0 &&
-
-
-
-
-
-
-
-
- }
- {header.length > 0 &&
-
-
-
-
-
-
-
-
- }
- {isGroupConstrained &&
-
-
-
- }
- {creator &&
-
-
-
-
- }
+
+
+
+
+ {displayName}
+
-
+ {this.renderHasGuestText(style)}
+ {purpose.length > 0 &&
+
+
+
+
+
+
+
+
+ }
+ {header.length > 0 &&
+
+
+
+
+
+
+
+
+ }
+ {isGroupConstrained &&
+
+
+
+ }
+ {creator &&
+
+
+
+
+ }
+
);
}
}
diff --git a/app/screens/channel_info/channel_info_header.test.js b/app/screens/channel_info/channel_info_header.test.js
index c865d1b3f..94ec048c5 100644
--- a/app/screens/channel_info/channel_info_header.test.js
+++ b/app/screens/channel_info/channel_info_header.test.js
@@ -43,6 +43,7 @@ describe('channel_info_header', () => {
isTeammateGuest: false,
hasGuests: false,
isGroupConstrained: false,
+ isLandscape: false,
};
test('should match snapshot', async () => {
diff --git a/app/screens/settings/section.js b/app/screens/settings/section.js
index b132af8d4..b1834cf77 100644
--- a/app/screens/settings/section.js
+++ b/app/screens/settings/section.js
@@ -87,6 +87,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
footer: {
marginTop: 10,
+ marginHorizontal: 15,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
diff --git a/app/screens/sso/index.js b/app/screens/sso/index.js
index f17246b5b..fa9c18f92 100644
--- a/app/screens/sso/index.js
+++ b/app/screens/sso/index.js
@@ -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),
};
}
diff --git a/app/screens/sso/sso.js b/app/screens/sso/sso.js
index 788a37196..51fb02f38 100644
--- a/app/screens/sso/sso.js
+++ b/app/screens/sso/sso.js
@@ -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 (
-
+
{content}
diff --git a/app/utils/general.js b/app/utils/general.js
index 4352bec08..6365e9380 100644
--- a/app/utils/general.js
+++ b/app/utils/general.js
@@ -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,
});
}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index a99e0da2e..70501c2c2 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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.",
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index cffbebf3f..d9dad2a12 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -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;
diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist
index 518277cf6..92c47118f 100644
--- a/ios/Mattermost/Info.plist
+++ b/ios/Mattermost/Info.plist
@@ -34,7 +34,7 @@
CFBundleVersion
- 238
+ 239
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist
index 9b9ffcd1b..b3855e6fc 100644
--- a/ios/MattermostShare/Info.plist
+++ b/ios/MattermostShare/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.24.0
CFBundleVersion
- 238
+ 239
NSAppTransportSecurity
NSAllowsArbitraryLoads
diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist
index 1553e2ccc..5b10c1430 100644
--- a/ios/MattermostTests/Info.plist
+++ b/ios/MattermostTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 238
+ 239
diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist
index 1a5d8a77b..f458ad0fe 100644
--- a/ios/NotificationService/Info.plist
+++ b/ios/NotificationService/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.24.0
CFBundleVersion
- 238
+ 239
NSExtension
NSExtensionPointIdentifier
diff --git a/patches/react-native-image-picker+0.28.1.patch b/patches/react-native-image-picker+0.28.1.patch
new file mode 100644
index 000000000..46b8ac1b5
--- /dev/null
+++ b/patches/react-native-image-picker+0.28.1.patch
@@ -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;
++ }
+ }
+ }
+
diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js
index b71c13094..ac90bd558 100644
--- a/share_extension/android/extension_post/extension_post.js
+++ b/share_extension/android/extension_post/extension_post.js
@@ -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 {
{
+ 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 (
+
+ {messageLengthRemaining}
+
+ );
+ };
+
render() {
const {formatMessage} = this.context.intl;
const {maxFileSize} = this.props;
@@ -610,6 +649,7 @@ export default class ExtensionPost extends PureComponent {
{this.renderBody()}
+ {this.renderMessageLengthRemaining()}
{this.renderTeamButton()}
{this.renderChannelButton()}
@@ -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,
diff --git a/share_extension/android/extension_post/extension_post.test.js b/share_extension/android/extension_post/extension_post.test.js
new file mode 100644
index 000000000..2422be80a
--- /dev/null
+++ b/share_extension/android/extension_post/extension_post.test.js
@@ -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(
+
+ );
+
+ 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();
+ });
+});