From 102faceb2b1a27eda5cdbd1dbf3c1b2fd29e2e4b Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 2 Aug 2018 15:44:30 -0400 Subject: [PATCH 01/12] Wrap set credentials for the keychain in a try/catch (#1962) * Wrap set credentials for the keychain in a try/catch * Only wrap the setGenericPassword method --- app/app.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/app.js b/app/app.js index 1096ec779..47ccfa556 100644 --- a/app/app.js +++ b/app/app.js @@ -161,6 +161,7 @@ export default class App { if (!currentUserId) { return; } + const username = `${deviceToken}, ${currentUserId}`; const password = `${token},${url}`; @@ -172,7 +173,11 @@ export default class App { // Only save to keychain if the url and token are set if (url && token) { - setGenericPassword(username, password); + try { + setGenericPassword(username, password); + } catch (e) { + console.warn('could not set credentials', e); //eslint-disable-line no-console + } } }; From 2fe9d8842a79dedfe7252b39d341f16aef561968 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Fri, 3 Aug 2018 22:52:43 +0800 Subject: [PATCH 02/12] [MM-11492] Add protection in accessing ".team_id" field when it's immediately accessed after the object is created (#1963) * add protection in accessing ".team_id" field when it's immediately accessed after the object is created. * remove lastChannelId since lastChannel is being checked already --- app/actions/views/channel.js | 7 +++++-- app/actions/views/root.js | 11 ++++++++--- app/components/reactions/index.js | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index be0f4b3cb..8365a9122 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -259,8 +259,11 @@ export function selectInitialChannel(teamId) { const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL && isGroupChannelVisible(myPreferences, lastChannel); - if (lastChannelId && myMembers[lastChannelId] && - (lastChannel.team_id === teamId || isDMVisible || isGMVisible)) { + if ( + myMembers[lastChannelId] && + lastChannel && + (lastChannel.team_id === teamId || isDMVisible || isGMVisible) + ) { handleSelectChannel(lastChannelId)(dispatch, getState); markChannelAsRead(lastChannelId)(dispatch, getState); return; diff --git a/app/actions/views/root.js b/app/actions/views/root.js index aea994297..5788e3613 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -55,10 +55,15 @@ export function loadFromPushNotification(notification) { const {data} = notification; const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams; const {currentChannelId, channels} = state.entities.channels; - const channelId = data ? data.channel_id : ''; - // when the notification does not have a team id is because its from a DM or GM - const teamId = data.team_id || currentTeamId; + let channelId = ''; + let teamId = currentTeamId; + if (data) { + channelId = data.channel_id; + + // when the notification does not have a team id is because its from a DM or GM + teamId = data.team_id || currentTeamId; + } // load any missing data const loading = []; diff --git a/app/components/reactions/index.js b/app/components/reactions/index.js index 48f0896cd..d29458654 100644 --- a/app/components/reactions/index.js +++ b/app/components/reactions/index.js @@ -22,7 +22,7 @@ function makeMapStateToProps() { return function mapStateToProps(state, ownProps) { const post = getPost(state, ownProps.postId); const channelId = post ? post.channel_id : ''; - const channel = getChannel(state, channelId); + const channel = getChannel(state, channelId) || {}; const teamId = channel.team_id; const channelIsArchived = channel.delete_at !== 0; From cf85c3c1e8a3f02e042a799d95bfe326faaae164 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 3 Aug 2018 13:18:21 -0400 Subject: [PATCH 03/12] Fix Android share extension and better handle errors (#1965) --- .../com/mattermost/share/RealPathUtil.java | 23 ++++++++++++-- assets/base/i18n/en.json | 1 + .../android/extension_post/extension_post.js | 31 ++++++++++++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/share/RealPathUtil.java b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java index b66ffd831..9515277df 100644 --- a/android/app/src/main/java/com/mattermost/share/RealPathUtil.java +++ b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.provider.OpenableColumns; import android.content.ContentUris; import android.content.ContentResolver; import android.os.Environment; @@ -95,15 +96,33 @@ public class RealPathUtil { public static String getPathFromSavingTempFile(Context context, final Uri uri) { File tmpFile; + String fileName = null; + + // Try and get the filename from the Uri try { - String fileName = uri.getLastPathSegment(); + Cursor returnCursor = + context.getContentResolver().query(uri, null, null, null, null); + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + fileName = returnCursor.getString(nameIndex); + } catch (Exception e) { + // just continue to get the filename with the last segment of the path + } + + try { + if (fileName == null) { + fileName = uri.getLastPathSegment().toString().trim(); + } + + File cacheDir = new File(context.getCacheDir(), "mmShare"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } String mimeType = getMimeType(uri.getPath()); - tmpFile = File.createTempFile("tmp", fileName, cacheDir); + tmpFile = new File(cacheDir, fileName); + tmpFile.createNewFile(); ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r"); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index ddefa5897..99def8192 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2432,6 +2432,7 @@ "mobile.error_handler.description": "\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n", "mobile.error_handler.title": "Unexpected error occurred", "mobile.extension.authentication_required": "Authentication required: Please first login using the app.", + "mobile.extension.file_error": "There was an error reading the file to be shared.\nPlease try again.", "mobile.extension.file_limit": "Sharing is limited to a maximum of 5 files.", "mobile.extension.max_file_size": "File attachments shared in Mattermost must be less than {size}.", "mobile.extension.permission": "Mattermost needs access to the device storage to share files.", diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index 18ce120ec..4829e8bec 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -268,10 +268,7 @@ export default class ExtensionPost extends PureComponent { const text = []; const files = []; let totalSize = 0; - - this.props.navigation.setParams({ - post: this.onPost, - }); + let error; for (let i = 0; i < items.length; i++) { const item = items[i]; @@ -280,8 +277,18 @@ export default class ExtensionPost extends PureComponent { text.push(item.value); break; default: { + let fileSize = {size: 0}; const fullPath = item.value; - const fileSize = await RNFetchBlob.fs.stat(fullPath); + try { + fileSize = await RNFetchBlob.fs.stat(fullPath); + } catch (e) { + const {formatMessage} = this.context.intl; + error = formatMessage({ + id: 'mobile.extension.file_error', + defaultMessage: 'There was an error reading the file to be shared.\nPlease try again.', + }); + break; + } let filename = fullPath.replace(/^.*[\\/]/, ''); let extension = filename.split('.').pop(); if (extension === filename) { @@ -305,7 +312,13 @@ export default class ExtensionPost extends PureComponent { const value = text.join('\n'); - this.setState({files, value, hasPermission: true, totalSize}); + if (!error) { + this.props.navigation.setParams({ + post: this.onPost, + }); + } + + this.setState({error, files, value, hasPermission: true, totalSize}); } }; @@ -476,7 +489,11 @@ export default class ExtensionPost extends PureComponent { render() { const {formatMessage} = this.context.intl; const {maxFileSize, token, url} = this.props; - const {hasPermission, files, totalSize} = this.state; + const {error, hasPermission, files, totalSize} = this.state; + + if (error) { + return this.renderErrorMessage(error); + } if (token && url) { if (hasPermission === false) { From c1317f0e94e1b340b62ddc72164d7f86051fef42 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Tue, 7 Aug 2018 20:11:50 +0800 Subject: [PATCH 04/12] add protection (emoty object) when accessing id after declaring an object. (#1970) --- app/components/combined_system_message/index.js | 2 +- .../sidebars/main/channels_list/switch_teams_button/index.js | 2 +- app/components/sidebars/settings/index.js | 2 +- app/screens/settings/notification_settings/index.js | 2 +- app/screens/timezone/index.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/combined_system_message/index.js b/app/components/combined_system_message/index.js index 7ccee01ea..70b14c446 100644 --- a/app/components/combined_system_message/index.js +++ b/app/components/combined_system_message/index.js @@ -15,7 +15,7 @@ function makeMapStateToProps() { const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames(); return (state, ownProps) => { - const currentUser = getCurrentUser(state); + const currentUser = getCurrentUser(state) || {}; const {allUserIds, allUsernames} = ownProps; return { currentUserId: currentUser.id, diff --git a/app/components/sidebars/main/channels_list/switch_teams_button/index.js b/app/components/sidebars/main/channels_list/switch_teams_button/index.js index aff581356..825e93234 100644 --- a/app/components/sidebars/main/channels_list/switch_teams_button/index.js +++ b/app/components/sidebars/main/channels_list/switch_teams_button/index.js @@ -9,7 +9,7 @@ import {getCurrentTeam, getMyTeamsCount, getChannelDrawerBadgeCount} from 'matte import SwitchTeamsButton from './switch_teams_button'; function mapStateToProps(state) { - const team = getCurrentTeam(state); + const team = getCurrentTeam(state) || {}; return { currentTeamId: team.id, diff --git a/app/components/sidebars/settings/index.js b/app/components/sidebars/settings/index.js index 4e4b8f263..0ce0f01f7 100644 --- a/app/components/sidebars/settings/index.js +++ b/app/components/sidebars/settings/index.js @@ -13,7 +13,7 @@ import {getDimensions} from 'app/selectors/device'; import SettingsSidebar from './settings_sidebar'; function mapStateToProps(state) { - const currentUser = getCurrentUser(state); + const currentUser = getCurrentUser(state) || {}; const status = getStatusForUserId(state, currentUser.id); return { diff --git a/app/screens/settings/notification_settings/index.js b/app/screens/settings/notification_settings/index.js index d41937991..71673b63d 100644 --- a/app/screens/settings/notification_settings/index.js +++ b/app/screens/settings/notification_settings/index.js @@ -15,7 +15,7 @@ import NotificationSettings from './notification_settings'; function mapStateToProps(state) { const config = getConfig(state); - const currentUser = getCurrentUser(state); + const currentUser = getCurrentUser(state) || {}; const currentUserStatus = getStatusForUserId(state, currentUser.id); const serverVersion = state.entities.general.serverVersion; const enableAutoResponder = isMinimumServerVersion(serverVersion, 4, 9) && config.ExperimentalEnableAutomaticReplies === 'true'; diff --git a/app/screens/timezone/index.js b/app/screens/timezone/index.js index 19e7280b4..fb2901c89 100644 --- a/app/screens/timezone/index.js +++ b/app/screens/timezone/index.js @@ -16,7 +16,7 @@ import Timezone from './timezone'; function mapStateToProps(state) { const timezones = getTimezones(state); - const currentUser = getCurrentUser(state); + const currentUser = getCurrentUser(state) || {}; const userTimezone = getUserTimezone(state, currentUser.id); return { From 8ab2373d7b1083f15877528d67c2944b823bc9c6 Mon Sep 17 00:00:00 2001 From: Sudheer Date: Tue, 7 Aug 2018 20:00:52 +0530 Subject: [PATCH 05/12] [MM-11051] Add a loading_palceholder when user info is loading (#1972) * MM-11051] Add a loading_palceholder when user info is loading * Update package json and lock --- .../loading_placeholder.test.js.snap | 153 +++ app/components/loading_placeholder/index.js | 28 + .../loading_placeholder.test.js | 19 + .../__snapshots__/channel_item.test.js.snap | 907 +++++++++++++++++- .../channel_item/channel_item.js | 23 +- .../channel_item/channel_item.test.js | 14 + .../main/channels_list/channel_item/index.js | 9 +- package-lock.json | 11 +- package.json | 3 +- 9 files changed, 1141 insertions(+), 26 deletions(-) create mode 100644 app/components/loading_placeholder/__snapshots__/loading_placeholder.test.js.snap create mode 100644 app/components/loading_placeholder/index.js create mode 100644 app/components/loading_placeholder/loading_placeholder.test.js diff --git a/app/components/loading_placeholder/__snapshots__/loading_placeholder.test.js.snap b/app/components/loading_placeholder/__snapshots__/loading_placeholder.test.js.snap new file mode 100644 index 000000000..f387312c0 --- /dev/null +++ b/app/components/loading_placeholder/__snapshots__/loading_placeholder.test.js.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoadingPlaceholder should match snapshot 1`] = ` +ShallowWrapper { + "length": 1, + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + 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 { + "accessible": true, + "allowFontScaling": true, + "children": Array [ + , + , + ], + "ellipsizeMode": "tail", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Loading", + "id": "loading_screen.loading", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "animationDelay": 300, + "minOpacity": 0.4, + "numberOfDots": 3, + "style": Object { + "fontSize": 28, + "letterSpacing": -3, + "lineHeight": 30, + "marginLeft": 5, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "accessible": true, + "allowFontScaling": true, + "children": Array [ + , + , + ], + "ellipsizeMode": "tail", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Loading", + "id": "loading_screen.loading", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "animationDelay": 300, + "minOpacity": 0.4, + "numberOfDots": 3, + "style": Object { + "fontSize": 28, + "letterSpacing": -3, + "lineHeight": 30, + "marginLeft": 5, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + }, +} +`; diff --git a/app/components/loading_placeholder/index.js b/app/components/loading_placeholder/index.js new file mode 100644 index 000000000..e2dc72fb9 --- /dev/null +++ b/app/components/loading_placeholder/index.js @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import AnimatedEllipsis from 'react-native-animated-ellipsis'; +import FormattedText from 'app/components/formatted_text'; + +import {Text} from 'react-native'; + +const LoadingPlaceholder = () => ( + + + + +); + +export default LoadingPlaceholder; diff --git a/app/components/loading_placeholder/loading_placeholder.test.js b/app/components/loading_placeholder/loading_placeholder.test.js new file mode 100644 index 000000000..96293f111 --- /dev/null +++ b/app/components/loading_placeholder/loading_placeholder.test.js @@ -0,0 +1,19 @@ +// 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'; +configure({adapter: new Adapter()}); + +import LoadingPlaceholder from './index.js'; + +describe('LoadingPlaceholder', () => { + test('should match snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); 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 0fcd3bb59..cba55a39a 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 @@ -121,7 +121,9 @@ ShallowWrapper { }, ] } - /> + > + + , @@ -200,7 +202,9 @@ ShallowWrapper { }, ] } - /> + > + + , "delayPressOut": 100, @@ -271,7 +275,9 @@ ShallowWrapper { }, ] } - /> + > + + , ], "style": Array [ @@ -334,7 +340,9 @@ ShallowWrapper { }, ] } - />, + > + + , undefined, ], "style": Array [ @@ -382,7 +390,7 @@ ShallowWrapper { "props": Object { "accessible": true, "allowFontScaling": true, - "children": undefined, + "children": , "ellipsizeMode": "tail", "numberOfLines": 1, "style": Array [ @@ -402,7 +410,15 @@ ShallowWrapper { ], }, "ref": null, - "rendered": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, "type": [Function], }, undefined, @@ -496,7 +512,9 @@ ShallowWrapper { }, ] } - /> + > + + , @@ -575,7 +593,9 @@ ShallowWrapper { }, ] } - /> + > + + , "delayPressOut": 100, @@ -646,7 +666,9 @@ ShallowWrapper { }, ] } - /> + > + + , ], "style": Array [ @@ -709,7 +731,9 @@ ShallowWrapper { }, ] } - />, + > + + , undefined, ], "style": Array [ @@ -757,7 +781,7 @@ ShallowWrapper { "props": Object { "accessible": true, "allowFontScaling": true, - "children": undefined, + "children": , "ellipsizeMode": "tail", "numberOfLines": 1, "style": Array [ @@ -777,7 +801,15 @@ ShallowWrapper { ], }, "ref": null, - "rendered": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, "type": [Function], }, undefined, @@ -818,3 +850,854 @@ ShallowWrapper { }, } `; + +exports[`ChannelItem should match snapshot for no displayName 1`] = ` +ShallowWrapper { + "length": 1, + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + 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": + + + + + + + + + , + }, + "ref": [Function], + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "activeOpacity": 0.85, + "children": + + + + + + + , + "delayPressOut": 100, + "onLongPress": [Function], + "onPress": [Function], + "underlayColor": "rgba(170,170,170,0.5)", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + undefined, + + + + + + , + ], + "style": Array [ + Object { + "flex": 1, + "flexDirection": "row", + "height": 44, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + undefined, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + undefined, + ], + "style": Array [ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "paddingLeft": 16, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "channelId": "channel_id", + "isActive": false, + "isArchived": false, + "isInfo": false, + "isUnread": true, + "membersCount": 1, + "size": 16, + "status": "online", + "teammateDeletedAt": 0, + "theme": Object { + "sidebarText": "#aaa", + "sidebarTextActiveBorder": "#aaa", + "sidebarTextActiveColor": "#aaa", + "sidebarTextHoverBg": "#aaa", + }, + "type": "O", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "accessible": true, + "allowFontScaling": true, + "children": , + "ellipsizeMode": "tail", + "numberOfLines": 1, + "style": Array [ + Object { + "color": "rgba(170,170,170,0.4)", + "flex": 1, + "fontSize": 14, + "fontWeight": "600", + "height": "100%", + "lineHeight": 44, + "paddingRight": 40, + "textAlignVertical": "center", + }, + Object { + "color": undefined, + }, + ], + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + undefined, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + "type": [Function], + }, + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": + + + + + + + + + , + }, + "ref": [Function], + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "activeOpacity": 0.85, + "children": + + + + + + + , + "delayPressOut": 100, + "onLongPress": [Function], + "onPress": [Function], + "underlayColor": "rgba(170,170,170,0.5)", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + undefined, + + + + + + , + ], + "style": Array [ + Object { + "flex": 1, + "flexDirection": "row", + "height": 44, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + undefined, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + undefined, + ], + "style": Array [ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "paddingLeft": 16, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "channelId": "channel_id", + "isActive": false, + "isArchived": false, + "isInfo": false, + "isUnread": true, + "membersCount": 1, + "size": 16, + "status": "online", + "teammateDeletedAt": 0, + "theme": Object { + "sidebarText": "#aaa", + "sidebarTextActiveBorder": "#aaa", + "sidebarTextActiveColor": "#aaa", + "sidebarTextHoverBg": "#aaa", + }, + "type": "O", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "accessible": true, + "allowFontScaling": true, + "children": , + "ellipsizeMode": "tail", + "numberOfLines": 1, + "style": Array [ + Object { + "color": "rgba(170,170,170,0.4)", + "flex": 1, + "fontSize": 14, + "fontWeight": "600", + "height": "100%", + "lineHeight": 44, + "paddingRight": 40, + "textAlignVertical": "center", + }, + Object { + "color": undefined, + }, + ], + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + undefined, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + "type": [Function], + }, + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + "context": Object { + "intl": Object { + "formatMessage": [MockFunction] { + "calls": Array [ + Array [ + Object { + "defaultMessage": "{displayName} (you)", + "id": "channel_header.directchannel.you", + }, + Object { + "displayname": "", + }, + ], + ], + }, + }, + }, + }, +} +`; 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 c87678098..cce3eca36 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 @@ -14,6 +14,7 @@ import {intlShape} from 'react-intl'; import Badge from 'app/components/badge'; import ChannelIcon from 'app/components/channel_icon'; +import LoadingPlaceholder from 'app/components/loading_placeholder'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -113,14 +114,6 @@ export default class ChannelItem extends PureComponent { const {intl} = this.context; - let channelDisplayName = displayName; - if (isMyUser) { - channelDisplayName = intl.formatMessage({ - id: 'channel_header.directchannel.you', - defaultMessage: '{displayName} (you)', - }, {displayname: displayName}); - } - const style = getStyleSheet(theme); const isActive = channelId === currentChannelId; @@ -140,6 +133,20 @@ export default class ChannelItem extends PureComponent { extraTextStyle = style.textUnread; } + let channelDisplayName = displayName; + if (isMyUser) { + channelDisplayName = intl.formatMessage({ + id: 'channel_header.directchannel.you', + defaultMessage: '{displayName} (you)', + }, {displayname: displayName}); + } + + if (!channelDisplayName) { + channelDisplayName = ( + + ); + } + let badge; if (mentions) { badge = ( diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js index 3ce307c00..7f4bd614f 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js @@ -45,4 +45,18 @@ describe('ChannelItem', () => { expect(wrapper).toMatchSnapshot(); }); + + test('should match snapshot for no displayName', () => { + const props = { + ...baseProps, + displayName: '', + }; + + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/app/components/sidebars/main/channels_list/channel_item/index.js b/app/components/sidebars/main/channels_list/channel_item/index.js index 2e0e1435d..6fbd29625 100644 --- a/app/components/sidebars/main/channels_list/channel_item/index.js +++ b/app/components/sidebars/main/channels_list/channel_item/index.js @@ -10,9 +10,10 @@ import { getMyChannelMember, shouldHideDefaultChannel, } from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; +import {displayUsername} from 'mattermost-redux/utils/user_utils'; import ChannelItem from './channel_item'; @@ -26,12 +27,15 @@ function makeMapStateToProps() { let isMyUser = false; let teammateDeletedAt = 0; + let displayName = channel.display_name; if (channel.type === General.DM_CHANNEL && channel.teammate_id) { isMyUser = channel.teammate_id === currentUserId; const teammate = getUser(state, channel.teammate_id); if (teammate && teammate.delete_at) { teammateDeletedAt = teammate.delete_at; } + const teammateNameDisplay = getTeammateNameDisplaySetting(state); + displayName = displayUsername(teammate, teammateNameDisplay, false); } const currentChannelId = getCurrentChannelId(state); @@ -57,10 +61,9 @@ function makeMapStateToProps() { if (member && member.notify_props) { showUnreadForMsgs = member.notify_props.mark_unread !== General.MENTION; } - return { currentChannelId, - displayName: channel.display_name, + displayName, fake: channel.fake, isChannelMuted: isChannelMuted(member), isMyUser, diff --git a/package-lock.json b/package-lock.json index ef5f3fe17..753ab065f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10108,8 +10108,8 @@ } }, "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#12353bc88f20dde53b6191747790c6a82b8dd527", - "from": "github:mattermost/mattermost-redux#12353bc88f20dde53b6191747790c6a82b8dd527", + "version": "github:mattermost/mattermost-redux#f13022064f1ed24a0d1c085f433490f9d40cfc2f", + "from": "github:mattermost/mattermost-redux#f13022064f1ed24a0d1c085f433490f9d40cfc2f", "requires": { "deep-equal": "1.0.1", "eslint-plugin-header": "1.2.0", @@ -14626,6 +14626,13 @@ "prop-types": "^15.5.10" } }, + "react-native-animated-ellipsis": { + "version": "github:sudheerDev/react-native-animated-ellipsis#326167b8e0fa3b6f0422c06be412bf177b725666", + "from": "github:sudheerDev/react-native-animated-ellipsis#326167b8e0fa3b6f0422c06be412bf177b725666", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-native-bottom-sheet": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-native-bottom-sheet/-/react-native-bottom-sheet-1.0.3.tgz", diff --git a/package.json b/package.json index 414e85b9f..0df80744e 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "intl": "1.2.5", "jail-monkey": "1.0.0", "jsc-android": "216113.0.3", - "mattermost-redux": "github:mattermost/mattermost-redux#12353bc88f20dde53b6191747790c6a82b8dd527", + "mattermost-redux": "github:mattermost/mattermost-redux#f13022064f1ed24a0d1c085f433490f9d40cfc2f", "mime-db": "1.33.0", "prop-types": "15.6.1", "react": "16.3.2", "react-intl": "2.4.0", "react-native": "github:enahum/react-native#mm", "react-native-animatable": "1.2.4", + "react-native-animated-ellipsis": "sudheerDev/react-native-animated-ellipsis.git#326167b8e0fa3b6f0422c06be412bf177b725666", "react-native-bottom-sheet": "1.0.3", "react-native-button": "2.3.0", "react-native-circular-progress": "0.2.0", From b01849be39ef89e944c1edcc2ca617dc133b06be Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 7 Aug 2018 10:32:04 -0400 Subject: [PATCH 06/12] MM-11477 Attempt to capture uncaught redux errors as Sentry breadcrumbs (#1974) --- .eslintrc.json | 3 +- .../mattermost-managed.android.js | 2 +- .../mattermost-managed.ios.js | 2 +- app/utils/error_handling.js | 20 +++-- app/utils/sentry/index.js | 77 +++++++++++++++++-- index.js | 1 - 6 files changed, 90 insertions(+), 15 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 818e88f2c..bd2345b99 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,7 +32,8 @@ "expect": true, "it": true, "jest": true, - "test": true + "test": true, + "__DEV__": true }, "rules": { "array-bracket-spacing": [2, "never"], diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js index 2e3f78adf..7aa71819c 100644 --- a/app/mattermost_managed/mattermost-managed.android.js +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -56,7 +56,7 @@ export default { } }, isTrustedDevice: () => { - if (__DEV__) { //eslint-disable-line no-undef + if (__DEV__) { return true; } diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js index 01c134993..1e0bb973b 100644 --- a/app/mattermost_managed/mattermost-managed.ios.js +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -55,7 +55,7 @@ export default { } }, isTrustedDevice: () => { - if (__DEV__) { //eslint-disable-line no-undef + if (__DEV__) { return true; } diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js index b0429c097..aa9d8d191 100644 --- a/app/utils/error_handling.js +++ b/app/utils/error_handling.js @@ -16,27 +16,35 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {purgeOfflineStore} from 'app/actions/views/root'; import { captureException, + captureJSException, initializeSentry, - LOGGER_JAVASCRIPT, LOGGER_NATIVE, } from 'app/utils/sentry'; import {app, store} from 'app/mattermost'; const errorHandler = (e, isFatal) => { - console.warn('Handling Javascript error ', e); // eslint-disable-line no-console + if (__DEV__ && !e && !isFatal) { + // react-native-exception-handler redirects console.error to call this, and React calls + // console.error without an exception when prop type validation fails, so this ends up + // being called with no arguments when the error handler is enabled in dev mode. + return; + } + + console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console + captureJSException(e, isFatal, store); + const {dispatch} = store; - captureException(e, LOGGER_JAVASCRIPT, store); - - const translations = app.getTranslations(); dispatch(closeWebSocket()); if (Client4.getUrl()) { dispatch(logError(e)); } - if (isFatal) { + if (isFatal && e instanceof Error) { + const translations = app.getTranslations(); + Alert.alert( translations['mobile.error_handler.title'], translations['mobile.error_handler.description'], diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js index af184abfc..80bfbaa26 100644 --- a/app/utils/sentry/index.js +++ b/app/utils/sentry/index.js @@ -17,6 +17,9 @@ export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning'; export const LOGGER_NATIVE = 'native'; export const LOGGER_REDUX = 'redux'; +export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error'; +export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error'; + export function initializeSentry() { if (!Config.SentryEnabled) { // Still allow Sentry to configure itself in case other code tries to call it @@ -44,12 +47,28 @@ function getDsn() { return ''; } -export function captureException(error, logger, store) { - if (error && logger && store) { - capture(() => { - Sentry.captureException(error, {logger}); - }, store); +export function captureJSException(error, isFatal, store) { + if (!error || !store) { + console.warn('captureJSException called with missing arguments', error, store); // eslint-disable-line no-console + return; } + + if (error instanceof Error) { + captureException(error, LOGGER_JAVASCRIPT, store); + } else { + captureNonErrorAsBreadcrumb(error, isFatal); + } +} + +export function captureException(error, logger, store) { + if (!error || !logger || !store) { + console.warn('captureException called with missing arguments', error, logger, store); // eslint-disable-line no-console + return; + } + + capture(() => { + Sentry.captureException(error, {logger}); + }, store); } export function captureExceptionWithoutState(err, logger) { @@ -74,6 +93,54 @@ export function captureMessage(message, logger, store) { } } +export function captureNonErrorAsBreadcrumb(obj, isFatal) { + if (!obj || typeof obj !== 'object') { + console.warning('Invalid object passed to captureNonErrorAsBreadcrumb', obj); // eslint-disable-line no-console + return; + } + + const isAppError = Boolean(obj.server_error_id); + + const breadcrumb = { + category: isAppError ? BREADCRUMB_UNCAUGHT_APP_ERROR : BREADCRUMB_UNCAUGHT_NON_ERROR, + data: { + isFatal: String(isFatal), + }, + level: 'warn', + }; + + if (obj.message) { + breadcrumb.message = obj.message; + } else if (obj.intl && obj.intl.defaultMessage) { + breadcrumb.message = breadcrumb.intl.defaultMessage; + } else { + breadcrumb.message = 'no message provided'; + } + + if (obj.server_error_id) { + breadcrumb.data.server_error_id = obj.server_error_id; + } + + if (obj.status_code) { + breadcrumb.data.status_code = obj.status_code; + } + + if (obj.url) { + const index = obj.url.indexOf('/'); + + if (index !== -1) { + breadcrumb.data.url = obj.url.substring(index); + } + } + + try { + Sentry.captureBreadcrumb(breadcrumb); + } catch (e) { + // Do nothing since this is only here to make sure we don't crash when handling an exception + console.warn('Failed to capture breadcrumb of non-error', e); // eslint-disable-line no-console + } +} + // Wrapper function to any calls to Sentry so that we can gather any necessary extra data // before sending. function capture(captureFunc, store) { diff --git a/index.js b/index.js index acb85c8b6..5746e3135 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,6 @@ if (Platform.OS === 'android') { /* /!* eslint-disable no-console *!/ -/!* eslint-disable no-undef *!/ if (__DEV__) { const modules = require.getModules(); const moduleIds = Object.keys(modules); From 21521a79be85451be5eea1618f1224aae92e220a Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 7 Aug 2018 10:48:51 -0400 Subject: [PATCH 07/12] MM-11477 Update react-native-sentry and sentry-javascript (#1975) --- package-lock.json | 96 +++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 753ab065f..523f08d27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2589,27 +2589,34 @@ } }, "@sentry/cli": { - "version": "1.30.4", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.30.4.tgz", - "integrity": "sha1-TS9PHbMZ8pUCYrjnW21okv4Xeco=", + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.34.0.tgz", + "integrity": "sha512-j2nYoci4yaA3dIL3Gheai2KKAtMdDGNipq7Ws9vO5SaG6uY0FQ0EXTxY0CAMPiNXdAXWstR5rdxPptyrhxPq7Q==", "requires": { - "https-proxy-agent": "^2.1.1", - "node-fetch": "^1.7.3", + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.1.2", "progress": "2.0.0", "proxy-from-env": "^1.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", + "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==" + } } }, "@sentry/wizard": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@sentry/wizard/-/wizard-0.9.6.tgz", - "integrity": "sha1-84+xsIgZ5rKAMFMOPTZOe2TJRZg=", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@sentry/wizard/-/wizard-0.10.3.tgz", + "integrity": "sha512-U+O8l6Kp9CTLJAgr/ChoRH16s/owGTF/dk/veTXgK9wMn+QKit6CEK/LAlah/nqgVx+CBvEVW+h4E8KIcSXMDg==", "requires": { "@sentry/cli": "^1.30.1", "chalk": "^2.3.1", "glob": "^7.1.2", "inquirer": "^5.1.0", "lodash": "^4.17.5", - "open": "^0.0.5", + "opn": "^5.3.0", "r2": "^2.0.0", "read-env": "^1.1.1", "xcode": "^1.0.0", @@ -2669,6 +2676,14 @@ "through": "^2.3.6" } }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "requires": { + "is-wsl": "^1.1.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -2701,9 +2716,9 @@ } }, "yargs": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", - "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "requires": { "cliui": "^4.0.0", "decamelize": "^1.1.1", @@ -2788,9 +2803,9 @@ } }, "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", "requires": { "es6-promisify": "^5.0.0" } @@ -8378,6 +8393,11 @@ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -13875,11 +13895,6 @@ "mimic-fn": "^1.0.0" } }, - "open": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" - }, "opn": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", @@ -14325,9 +14340,9 @@ }, "dependencies": { "node-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", - "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", + "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==" } } }, @@ -14399,9 +14414,9 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raven-js": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.24.2.tgz", - "integrity": "sha512-Dy/FHDxuo5pXywVf8Nrs5utB2juMATpkxWGqHjVbpFD3m8CaWYLvkB5SEXjWFUZ5GvUsrBVVQ+Dfcp0x6Z7SOg==" + "version": "3.26.4", + "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.26.4.tgz", + "integrity": "sha512-5VmC3IWhTQJkaiQaCY0S5V8za4bpUgbbuVT1MkDH7JVqgu8CPQ750XaFF8BVRbLV9F5nvoz7n0UT0CKteDuZAg==" }, "raw-body": { "version": "2.3.2", @@ -14843,11 +14858,11 @@ "integrity": "sha512-kR6s97ubi1QevRWRC8RQ26NoD6EgLt36caY25GKBt9eZ+l9Qmm2JHti2cEK153cxLIMbD04Xv4+LjDhcNjxh/w==" }, "react-native-sentry": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/react-native-sentry/-/react-native-sentry-0.36.0.tgz", - "integrity": "sha1-HGbXKvVqqAHaqlVpwJ/H0EvqZUQ=", + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/react-native-sentry/-/react-native-sentry-0.38.3.tgz", + "integrity": "sha512-+RsSsLdjSf7MmIOkjoMqIfGTOISG3ym2hcldMaxCfa9HRTVXL7dne4naWEqFbs32C9M6KgXe/RZlp5cDaELLsA==", "requires": { - "@sentry/wizard": "^0.9.5", + "@sentry/wizard": "^0.10.2", "raven-js": "^3.24.2" } }, @@ -15020,11 +15035,18 @@ } }, "read-env": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/read-env/-/read-env-1.1.1.tgz", - "integrity": "sha512-yzGQb8P7N8zf513nRu+kMiQPJlmADWqS7Qn2pd6aAvRcrMCxuFvRX1KL8KU0ckdWVqoTX7Qpj10U+Dx7cVL/LA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/read-env/-/read-env-1.2.0.tgz", + "integrity": "sha512-29PX3GtJ9nUWsM8UckqJVdrqJzv8yWPDH3lxk2+CBFN3mJ1gJ42dluzxqmADCG7H0jL26G53G5+CwAqQph2Ctw==", "requires": { - "camelcase": "^4.1.0" + "camelcase": "5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + } } }, "read-pkg": { @@ -15773,9 +15795,9 @@ } }, "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "requires": { "symbol-observable": "1.0.1" }, diff --git a/package.json b/package.json index 0df80744e..9c4195618 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "react-native-permissions": "1.1.1", "react-native-safe-area": "0.2.3", "react-native-section-list-get-item-layout": "2.2.2", - "react-native-sentry": "0.36.0", + "react-native-sentry": "0.38.3", "react-native-slider": "0.11.0", "react-native-status-bar-size": "0.3.3", "react-native-svg": "6.3.1", From 1c758e22877ee6602b6834a7d3cf4d6057824a47 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Wed, 8 Aug 2018 00:18:09 +0800 Subject: [PATCH 08/12] [MM-10918] Filter system_remove_channel messages of a current user based on hiding/showing join/leave messages (#1955) * filter system_remove_channel messages of a current user based on hiding/showing join/leave messages * update mattermost-redux * makes the remove_from_channel/team shows after leave_channel/team * update per comments * revert change on mattermost-redux commit to prevent conflict with existing PR --- .../combined_system_message.js | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/components/combined_system_message/combined_system_message.js b/app/components/combined_system_message/combined_system_message.js index 52ee86612..f3841615e 100644 --- a/app/components/combined_system_message/combined_system_message.js +++ b/app/components/combined_system_message/combined_system_message.js @@ -304,14 +304,24 @@ export default class CombinedSystemMessage extends React.PureComponent { ); } + renderMessage(postType, userIds, actorId, style) { + return ( + + {this.renderFormattedMessage(postType, userIds, actorId, {baseText: style.baseText, linkText: style.linkText})} + + ); + } + render() { const { + currentUserId, messageData, theme, } = this.props; const style = getStyleSheet(theme); const content = []; + const removedUserIds = []; for (const message of messageData) { const { postType, @@ -319,23 +329,29 @@ export default class CombinedSystemMessage extends React.PureComponent { } = message; let userIds = message.userIds; - if (!this.props.showJoinLeave && actorId !== this.props.currentUserId) { - const affectsCurrentUser = userIds.indexOf(this.props.currentUserId) !== -1; + if (!this.props.showJoinLeave && actorId !== currentUserId) { + const affectsCurrentUser = userIds.indexOf(currentUserId) !== -1; if (affectsCurrentUser) { // Only show the message that the current user was added, etc - userIds = [this.props.currentUserId]; + userIds = [currentUserId]; } else { // Not something the current user did or was affected by continue; } } - content.push( - - {this.renderFormattedMessage(postType, userIds, actorId, {baseText: style.baseText, linkText: style.linkText})} - - ); + if (postType === REMOVE_FROM_CHANNEL) { + removedUserIds.push(...userIds); + continue; + } + + content.push(this.renderMessage(postType, userIds, actorId, style)); + } + + if (removedUserIds.length > 0) { + const uniqueRemovedUserIds = removedUserIds.filter((id, index, arr) => arr.indexOf(id) === index); + content.push(this.renderMessage(REMOVE_FROM_CHANNEL, uniqueRemovedUserIds, currentUserId, style)); } return ( From 979b80d34f80009d4631ba27386aeff46dd9449d Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 7 Aug 2018 13:05:59 -0400 Subject: [PATCH 09/12] MM-11477 Fix error in breadcrumb generation and parse URL correctly (#1977) --- app/utils/sentry/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js index 80bfbaa26..be665e8d8 100644 --- a/app/utils/sentry/index.js +++ b/app/utils/sentry/index.js @@ -112,7 +112,7 @@ export function captureNonErrorAsBreadcrumb(obj, isFatal) { if (obj.message) { breadcrumb.message = obj.message; } else if (obj.intl && obj.intl.defaultMessage) { - breadcrumb.message = breadcrumb.intl.defaultMessage; + breadcrumb.message = obj.intl.defaultMessage; } else { breadcrumb.message = 'no message provided'; } @@ -126,10 +126,10 @@ export function captureNonErrorAsBreadcrumb(obj, isFatal) { } if (obj.url) { - const index = obj.url.indexOf('/'); + const match = (/^(?:https?:\/\/)[^/]+(\/.*)$/); - if (index !== -1) { - breadcrumb.data.url = obj.url.substring(index); + if (match && match.length >= 2) { + breadcrumb.data.url = match[1]; } } From 34a37cef05cf30c9735de740af50a67c8e6abe63 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 7 Aug 2018 13:11:48 -0400 Subject: [PATCH 10/12] MM-11477 Ensure client calls are wrapped with error handling (#1976) --- app/actions/views/login.js | 26 ++++++++++++++++++-------- app/actions/views/root.js | 4 +++- app/app.js | 8 +++++++- app/fetch_preconfig.js | 1 + app/mattermost.js | 9 +++++++-- app/screens/sso/sso.js | 8 +++++++- app/utils/push_notifications.js | 21 +++++++++++++-------- 7 files changed, 56 insertions(+), 21 deletions(-) diff --git a/app/actions/views/login.js b/app/actions/views/login.js index 4d6c06132..1386617b8 100644 --- a/app/actions/views/login.js +++ b/app/actions/views/login.js @@ -6,7 +6,7 @@ import {GeneralTypes} from 'mattermost-redux/action_types'; import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; import {Client4} from 'mattermost-redux/client'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId, getSessions} from 'mattermost-redux/selectors/entities/users'; import {ViewTypes} from 'app/constants'; import {app} from 'app/mattermost'; @@ -73,15 +73,25 @@ export function getSession() { const {credentials} = state.entities.general; const token = credentials && credentials.token; - if (currentUserId && token) { - const session = await Client4.getSessions(currentUserId, token); - if (Array.isArray(session) && session[0]) { - const s = session[0]; - return s.expires_at; - } + if (!currentUserId || !token) { + return 0; } - return false; + let sessions; + try { + sessions = await getSessions(currentUserId); + } catch (e) { + console.warn('Failed to get current session', e); // eslint-disable-line no-console + return 0; + } + + if (!Array.isArray(sessions)) { + return 0; + } + + const session = sessions.find((s) => s.token === token); + + return session && session.expires_at ? session.expires_at : 0; }; } diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 5788e3613..8fe81acd4 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -102,7 +102,9 @@ export function purgeOfflineStore() { return {type: General.OFFLINE_STORE_PURGE}; } -export function createPost(post) { +// A non-optimistic version of the createPost action in mattermost-redux with the file handling +// removed since it's not needed. +export function createPostForNotificationReply(post) { return (dispatch, getState) => { const state = getState(); const currentUserId = state.entities.users.currentUserId; diff --git a/app/app.js b/app/app.js index 47ccfa556..91818f1d9 100644 --- a/app/app.js +++ b/app/app.js @@ -259,7 +259,13 @@ export default class App { if (this.token && this.url) { screen = 'Channel'; tracker.initialLoad = Date.now(); - dispatch(loadMe()); + + try { + dispatch(loadMe()); + } catch (e) { + // Fall through since we should have a previous version of the current user because we have a token + console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console + } } switch (screen) { diff --git a/app/fetch_preconfig.js b/app/fetch_preconfig.js index 9a8e02451..281726e1c 100644 --- a/app/fetch_preconfig.js +++ b/app/fetch_preconfig.js @@ -42,6 +42,7 @@ Client4.doFetchWithResponse = async (url, options) => { } throw { + message: 'Received invalid response from the server.', intl: { id: 'mobile.request.invalid_response', defaultMessage: 'Received invalid response from the server.', diff --git a/app/mattermost.js b/app/mattermost.js index 704a3b939..e7bbc5f11 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -142,8 +142,13 @@ const handleLogout = () => { const restartApp = async () => { Navigation.dismissModal({animationType: 'none'}); - await store.dispatch(loadConfigAndLicense()); - await store.dispatch(loadMe()); + try { + await store.dispatch(loadConfigAndLicense()); + await store.dispatch(loadMe()); + } catch (e) { + console.warn('Failed to load initial data while restarting', e); // eslint-disable-line no-console + } + launchChannel(); }; diff --git a/app/screens/sso/sso.js b/app/screens/sso/sso.js index 736958af6..2eb19f45d 100644 --- a/app/screens/sso/sso.js +++ b/app/screens/sso/sso.js @@ -187,12 +187,18 @@ class SSO extends PureComponent { setStoreFromLocalData({url: this.props.serverUrl, token}). then(handleSuccessfulLogin). then(getSession). - then(this.goToLoadTeam); + then(this.goToLoadTeam). + catch(this.onLoadEndError); } }); } }; + onLoadEndError = (e) => { + console.warn('Failed to set store from local data', e); // eslint-disable-line no-console + this.setState({error: e.message}); + } + renderLoading = () => { return ; }; diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index f4662a214..6249c7509 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -16,7 +16,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {ViewTypes} from 'app/constants'; import {retryGetPostsAction} from 'app/actions/views/channel'; import { - createPost, + createPostForNotificationReply, loadFromPushNotification, } from 'app/actions/views/root'; @@ -136,15 +136,20 @@ export const onPushNotificationReply = (data, text, badge, completed) => { } retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - dispatch(createPost(post)).then(() => { - dispatch(markChannelAsRead(data.channel_id)); + dispatch(createPostForNotificationReply(post)). + then(() => { + dispatch(markChannelAsRead(data.channel_id)); - if (badge >= 0) { - PushNotifications.setApplicationIconBadgeNumber(badge); - } + if (badge >= 0) { + PushNotifications.setApplicationIconBadgeNumber(badge); + } - app.setReplyNotificationData(null); - }).then(completed); + app.setReplyNotificationData(null); + }). + then(completed). + catch((e) => { + console.warn('Failed to send reply to push notification', e); // eslint-disable-line no-console + }); } else { app.setReplyNotificationData({ data, From e1ba40218fd9e8fb8406c641edd880b3592c9329 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 7 Aug 2018 14:09:16 -0400 Subject: [PATCH 11/12] Bump iOS build number to 129 (#1979) --- ios/Mattermost.xcodeproj/project.pbxproj | 4 ++-- ios/Mattermost/Info.plist | 2 +- ios/MattermostShare/Info.plist | 2 +- ios/MattermostTests/Info.plist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 6122e5a75..8a10b0ea6 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -2531,7 +2531,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2581,7 +2581,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 2a3ee0f13..90131f7ea 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 128 + 129 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 5295ece27..cd7d4e3d1 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -23,7 +23,7 @@ CFBundleShortVersionString 1.11.0 CFBundleVersion - 128 + 129 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index 86e32f0a0..8ba6c5668 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 128 + 129 From bca46b23fe6740fc4b74614db58362b199e79a79 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 7 Aug 2018 14:29:59 -0400 Subject: [PATCH 12/12] Bump Android build number to 129 (#1980) * Fix Sentry module in Android MainApplication * Bump Android build number to 129 --- android/app/build.gradle | 2 +- .../src/main/java/com/mattermost/rnbeta/MainApplication.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 802d77cb4..7578c5b85 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -113,7 +113,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion 21 targetSdkVersion 23 - versionCode 128 + versionCode 129 versionName "1.11.0" ndk { abiFilters "armeabi-v7a", "x86" diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index b1da95ad6..7ef74b51b 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -66,7 +66,7 @@ public class MainApplication extends NavigationApplication implements INotificat new JailMonkeyPackage(), new RNFetchBlobPackage(), new MattermostPackage(this), - new RNSentryPackage(this), + new RNSentryPackage(), new ReactNativeExceptionHandlerPackage(), new ReactNativeYouTube(), new ReactVideoPackage(),