From 2d695c22698df5a31e80885397213407e02615ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 3 Sep 2018 17:49:59 +0200 Subject: [PATCH 1/7] MM-11728: Reload channel on archived channel errors (on edit and archive channel) (#2064) --- app/screens/channel_info/channel_info.js | 4 ++++ app/screens/channel_info/index.js | 2 ++ app/screens/edit_channel/edit_channel.js | 8 ++++++-- app/screens/edit_channel/index.js | 3 ++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 0567d0626..64f94917e 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -29,6 +29,7 @@ export default class ChannelInfo extends PureComponent { closeGMChannel: PropTypes.func.isRequired, deleteChannel: PropTypes.func.isRequired, getChannelStats: PropTypes.func.isRequired, + getChannel: PropTypes.func.isRequired, leaveChannel: PropTypes.func.isRequired, loadChannelsByTeamName: PropTypes.func.isRequired, favoriteChannel: PropTypes.func.isRequired, @@ -202,6 +203,9 @@ export default class ChannelInfo extends PureComponent { displayName: channel.display_name, } ); + if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') { + this.props.actions.getChannel(channel.id); + } } else { this.close(); } diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index 81c95e8db..4f3c391a8 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -7,6 +7,7 @@ import {connect} from 'react-redux'; import { favoriteChannel, getChannelStats, + getChannel, deleteChannel, unfavoriteChannel, updateChannelNotifyProps, @@ -89,6 +90,7 @@ function mapDispatchToProps(dispatch) { closeGMChannel, deleteChannel, getChannelStats, + getChannel, leaveChannel, loadChannelsByTeamName, favoriteChannel, diff --git a/app/screens/edit_channel/edit_channel.js b/app/screens/edit_channel/edit_channel.js index 22d04d8c8..917359ea9 100644 --- a/app/screens/edit_channel/edit_channel.js +++ b/app/screens/edit_channel/edit_channel.js @@ -52,6 +52,7 @@ export default class EditChannel extends PureComponent { static propTypes = { actions: PropTypes.shape({ patchChannel: PropTypes.func.isRequired, + getChannel: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, }), navigator: PropTypes.object.isRequired, @@ -204,7 +205,7 @@ export default class EditChannel extends PureComponent { return {error: formatMessage(messages.name_lowercase)}; }; - onUpdateChannel = () => { + onUpdateChannel = async () => { Keyboard.dismiss(); const {displayName, channelURL, purpose, header} = this.state; const {channel: {id, type}} = this.props; @@ -230,7 +231,10 @@ export default class EditChannel extends PureComponent { } } - this.props.actions.patchChannel(id, channel); + const data = await this.props.actions.patchChannel(id, channel); + if (data.error && data.error.server_error_id === 'store.sql_channel.update.archived_channel.app_error') { + this.props.actions.getChannel(id); + } }; onNavigatorEvent = (event) => { diff --git a/app/screens/edit_channel/index.js b/app/screens/edit_channel/index.js index e98433c02..4a0eb2182 100644 --- a/app/screens/edit_channel/index.js +++ b/app/screens/edit_channel/index.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams'; -import {patchChannel} from 'mattermost-redux/actions/channels'; +import {patchChannel, getChannel} from 'mattermost-redux/actions/channels'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {setChannelDisplayName} from 'app/actions/views/channel'; @@ -33,6 +33,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ patchChannel, + getChannel, setChannelDisplayName, }, dispatch), }; From 0e6ffab1e0aa047ed1e63d3b64414b94f5433da0 Mon Sep 17 00:00:00 2001 From: Sudheer Date: Tue, 4 Sep 2018 18:18:22 +0530 Subject: [PATCH 2/7] MM-11603 Fix select_team ui to have failed request scenario (#2063) * MM-11603 Fix select_team ui to have failed request scenario * Change error title and description --- app/components/failed_network_action/index.js | 21 +- .../__snapshots__/select_team.test.js.snap | 609 ++++++++++++++++++ app/screens/select_team/index.js | 2 +- app/screens/select_team/select_team.js | 37 +- app/screens/select_team/select_team.test.js | 97 +++ assets/base/i18n/en.json | 1 + 6 files changed, 758 insertions(+), 9 deletions(-) create mode 100644 app/screens/select_team/__snapshots__/select_team.test.js.snap create mode 100644 app/screens/select_team/select_team.test.js diff --git a/app/components/failed_network_action/index.js b/app/components/failed_network_action/index.js index f2f262f37..9126258f8 100644 --- a/app/components/failed_network_action/index.js +++ b/app/components/failed_network_action/index.js @@ -14,6 +14,19 @@ export default class FailedNetworkAction extends PureComponent { static propTypes = { onRetry: PropTypes.func, theme: PropTypes.object.isRequired, + errorTitle: PropTypes.object, + errorDescription: PropTypes.object, + }; + + static defaultProps = { + errorTitle: { + id: 'mobile.failed_network_action.title', + defaultMessage: 'No internet connection', + }, + errorDescription: { + id: 'mobile.failed_network_action.description', + defaultMessage: 'There seems to be a problem with your internet connection. Make sure you have an active connection and try again.', + }, }; render() { @@ -28,13 +41,13 @@ export default class FailedNetworkAction extends PureComponent { width={76} /> {onRetry && diff --git a/app/screens/select_team/__snapshots__/select_team.test.js.snap b/app/screens/select_team/__snapshots__/select_team.test.js.snap new file mode 100644 index 000000000..792a26e69 --- /dev/null +++ b/app/screens/select_team/__snapshots__/select_team.test.js.snap @@ -0,0 +1,609 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectTeam should match snapshot for fail of teams 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 { + "errorDescription": Object { + "defaultMessage": "Make sure you have an active connection and try again.", + "id": "mobile.failed_network_action.shortDescription", + }, + "errorTitle": Object { + "defaultMessage": "Team Not Found", + "id": "error.team_not_found.title", + }, + "onRetry": [Function], + "theme": Object {}, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "errorDescription": Object { + "defaultMessage": "Make sure you have an active connection and try again.", + "id": "mobile.failed_network_action.shortDescription", + }, + "errorTitle": Object { + "defaultMessage": "Team Not Found", + "id": "error.team_not_found.title", + }, + "onRetry": [Function], + "theme": Object {}, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + }, +} +`; + +exports[`SelectTeam should match snapshot for teams 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": Array [ + , + + + + + + , + , + ], + "style": Object { + "backgroundColor": undefined, + "flex": 1, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + , + , + ], + "style": Object { + "alignItems": "center", + "flexDirection": "row", + "marginHorizontal": 16, + "marginTop": 20, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginRight": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Open teams you can join", + "id": "mobile.select_team.join_open", + "style": Object { + "color": undefined, + "fontSize": 13, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": undefined, + "height": 1, + "width": "100%", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "data": Array [ + Object { + "id": "kemjcpu9bi877yegqjs18ndp4r", + "invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae", + "name": "test", + }, + ], + "disableVirtualization": false, + "horizontal": false, + "initialNumToRender": 10, + "keyExtractor": [Function], + "maxToRenderPerBatch": 10, + "numColumns": 1, + "onEndReachedThreshold": 2, + "renderItem": [Function], + "scrollEventThrottle": 50, + "updateCellsBatchingPeriod": 50, + "viewabilityConfig": Object { + "viewAreaCoveragePercentThreshold": 3, + "waitForInteraction": false, + }, + "windowSize": 21, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + + + + , + , + ], + "style": Object { + "backgroundColor": undefined, + "flex": 1, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + , + , + ], + "style": Object { + "alignItems": "center", + "flexDirection": "row", + "marginHorizontal": 16, + "marginTop": 20, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginRight": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Open teams you can join", + "id": "mobile.select_team.join_open", + "style": Object { + "color": undefined, + "fontSize": 13, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": undefined, + "height": 1, + "width": "100%", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "data": Array [ + Object { + "id": "kemjcpu9bi877yegqjs18ndp4r", + "invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae", + "name": "test", + }, + ], + "disableVirtualization": false, + "horizontal": false, + "initialNumToRender": 10, + "keyExtractor": [Function], + "maxToRenderPerBatch": 10, + "numColumns": 1, + "onEndReachedThreshold": 2, + "renderItem": [Function], + "scrollEventThrottle": 50, + "updateCellsBatchingPeriod": 50, + "viewabilityConfig": Object { + "viewAreaCoveragePercentThreshold": 3, + "waitForInteraction": false, + }, + "windowSize": 21, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + }, +} +`; diff --git a/app/screens/select_team/index.js b/app/screens/select_team/index.js index 91c6843da..8978cd48d 100644 --- a/app/screens/select_team/index.js +++ b/app/screens/select_team/index.js @@ -28,7 +28,7 @@ function mapStateToProps(state) { } return { - teamsRequest: state.requests.teams.getMyTeams, + teamsRequest: state.requests.teams.getTeams, teams: Object.values(getJoinableTeams(state)).sort(sortTeams), currentChannelId: getCurrentChannelId(state), joinTeamRequest: state.requests.teams.joinTeam, diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js index 82d369cce..5a5f9e2db 100644 --- a/app/screens/select_team/select_team.js +++ b/app/screens/select_team/select_team.js @@ -15,6 +15,7 @@ import { import {RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import FailedNetworkAction from 'app/components/failed_network_action'; import FormattedText from 'app/components/formatted_text'; import Loading from 'app/components/loading'; import StatusBar from 'app/components/status_bar'; @@ -28,6 +29,16 @@ const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS; const TEAMS_PER_PAGE = 200; +const errorTitle = { + id: 'error.team_not_found.title', + defaultMessage: 'Team Not Found', +}; + +const errorDescription = { + id: 'mobile.failed_network_action.shortDescription', + defaultMessage: 'Make sure you have an active connection and try again.', +}; + export default class SelectTeam extends PureComponent { static propTypes = { actions: PropTypes.shape({ @@ -44,6 +55,7 @@ export default class SelectTeam extends PureComponent { userWithoutTeams: PropTypes.bool, teams: PropTypes.array.isRequired, theme: PropTypes.object, + teamsRequest: PropTypes.object.isRequired, }; constructor(props) { @@ -57,9 +69,7 @@ export default class SelectTeam extends PureComponent { } componentDidMount() { - this.props.actions.getTeams(0, TEAMS_PER_PAGE).then(() => { - this.buildData(this.props); - }); + this.getTeams(); } componentWillReceiveProps(nextProps) { @@ -77,6 +87,14 @@ export default class SelectTeam extends PureComponent { } } + getTeams = () => { + this.setState({loading: true}); + this.props.actions.getTeams(0, TEAMS_PER_PAGE).then(() => { + this.setState({loading: false}); + this.buildData(this.props); + }); + } + buildData = (props) => { if (props.teams.length) { this.setState({teams: props.teams}); @@ -213,10 +231,21 @@ export default class SelectTeam extends PureComponent { const {teams} = this.state; const styles = getStyleSheet(theme); - if (this.state.joining) { + if (this.state.joining || this.state.loading) { return ; } + if (this.props.teamsRequest.status === RequestStatus.FAILURE) { + return ( + + ); + } + return ( diff --git a/app/screens/select_team/select_team.test.js b/app/screens/select_team/select_team.test.js new file mode 100644 index 000000000..90f7ec1f8 --- /dev/null +++ b/app/screens/select_team/select_team.test.js @@ -0,0 +1,97 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {configure, shallow} from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import {RequestStatus} from 'mattermost-redux/constants'; +configure({adapter: new Adapter()}); + +import SelectTeam from './select_team.js'; + +jest.mock('rn-fetch-blob', () => ({ + fs: { + dirs: { + DocumentDir: () => jest.fn(), + CacheDir: () => jest.fn(), + }, + }, +})); + +jest.mock('rn-fetch-blob/fs', () => ({ + dirs: { + DocumentDir: () => jest.fn(), + CacheDir: () => jest.fn(), + }, +})); + +jest.mock('app/utils/theme', () => { + const original = require.requireActual('app/utils/theme'); + return { + ...original, + changeOpacity: jest.fn(), + }; +}); + +const getTeams = async () => { + return { + error: {}, + }; +}; + +describe('SelectTeam', () => { + const actions = { + getTeams, + handleTeamChange: jest.fn(), + joinTeam: jest.fn(), + logout: jest.fn(), + markChannelAsRead: jest.fn(), + }; + + const baseProps = { + actions, + currentChannelId: 'someId', + currentUrl: 'test', + joinTeamRequest: {}, + navigator: { + setOnNavigatorEvent: jest.fn(), + }, + userWithoutTeams: false, + teams: [], + theme: {}, + teamsRequest: { + status: RequestStatus.FAILURE, + }, + }; + + test('should match snapshot for fail of teams', async () => { + const wrapper = shallow( + , + ); + expect(wrapper.state('loading')).toEqual(true); + await getTeams(); + expect(wrapper.state('loading')).toEqual(false); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot for teams', async () => { + const props = { + ...baseProps, + teams: [{ + id: 'kemjcpu9bi877yegqjs18ndp4r', + invite_id: 'ojsnudhqzbfzpk6e4n6ip1hwae', + name: 'test', + }], + teamsRequest: { + status: RequestStatus.SUCCESS, + }, + }; + + const wrapper = shallow( + , + ); + await getTeams(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 09d63872d..b7dc7a1c0 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2455,6 +2455,7 @@ "mobile.failed_network_action.description": "There seems to be a problem with your internet connection. Make sure you have an active connection and try again.", "mobile.failed_network_action.retry": "Try Again", "mobile.failed_network_action.title": "No internet connection", + "mobile.failed_network_action.shortDescription": "Make sure you have an active connection and try again.", "mobile.file_upload.browse": "Browse Files", "mobile.file_upload.camera": "Take Photo or Video", "mobile.file_upload.library": "Photo Library", From 9deeb86e1e511e8bf8090f3940e361f8c3ca4f09 Mon Sep 17 00:00:00 2001 From: Dmitry Samuylov Date: Tue, 4 Sep 2018 10:52:16 -0400 Subject: [PATCH 3/7] bug fix: use user's timezone setting for search date flags (#2067) --- app/screens/search/index.js | 9 +++++++++ app/screens/search/search.js | 5 ++--- app/utils/timezone.js | 9 +++++++-- package-lock.json | 11 +++++++++-- package.json | 1 + 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/screens/search/index.js b/app/screens/search/index.js index fd0a060b1..c4cf4f39d 100644 --- a/app/screens/search/index.js +++ b/app/screens/search/index.js @@ -10,11 +10,14 @@ import {getCurrentChannelId, filterPostIds} from 'mattermost-redux/selectors/ent import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {isLandscape} from 'app/selectors/device'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; import {handleSearchDraftChanged} from 'app/actions/views/search'; +import {getDeviceUtcOffset, getUtcOffsetForTimeZone, isTimezoneEnabled} from 'app/utils/timezone'; import Search from './search'; @@ -30,6 +33,11 @@ function makeMapStateToProps() { const {recent} = state.entities.search; const {searchPosts: searchRequest} = state.requests.search; + const currentUser = getCurrentUser(state); + const enableTimezone = isTimezoneEnabled(state); + const userCurrentTimezone = enableTimezone ? getUserCurrentTimezone(currentUser.timezone) : ''; + const timezoneOffsetInSeconds = (userCurrentTimezone.length > 0 ? getUtcOffsetForTimeZone(userCurrentTimezone) : getDeviceUtcOffset()) * 60; + const serverVersion = state.entities.general.serverVersion; const enableDateSuggestion = isMinimumServerVersion(serverVersion, 5, 3); @@ -43,6 +51,7 @@ function makeMapStateToProps() { searchingStatus: searchRequest.status, theme: getTheme(state), enableDateSuggestion, + timezoneOffsetInSeconds, }; }; } diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 58929854f..2b3bc2736 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -31,7 +31,6 @@ import SearchBar from 'app/components/search_bar'; import StatusBar from 'app/components/status_bar'; import mattermostManaged from 'app/mattermost_managed'; import {preventDoubleTap} from 'app/utils/tap'; -import {getDeviceUtcOffset} from 'app/utils/timezone'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDisplayName from './channel_display_name'; @@ -65,6 +64,7 @@ export default class Search extends PureComponent { searchingStatus: PropTypes.string, theme: PropTypes.object.isRequired, enableDateSuggestion: PropTypes.bool, + timezoneOffsetInSeconds: PropTypes.number.isRequired, }; static defaultProps = { @@ -469,8 +469,7 @@ export default class Search extends PureComponent { }); // timezone offset in seconds - const timeZoneOffset = getDeviceUtcOffset() * 60; - actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: timeZoneOffset}, true); + actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: this.props.timezoneOffsetInSeconds}, true); }; handleSearchButtonPress = preventDoubleTap((text) => { diff --git a/app/utils/timezone.js b/app/utils/timezone.js index 8498efecf..a91b961d7 100644 --- a/app/utils/timezone.js +++ b/app/utils/timezone.js @@ -5,13 +5,18 @@ import DeviceInfo from 'react-native-device-info'; import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import moment from 'moment-timezone'; + export function getDeviceTimezone() { return DeviceInfo.getTimezone(); } export function getDeviceUtcOffset() { - const reverseOffsetInMinutes = new Date().getTimezoneOffset(); - return -reverseOffsetInMinutes; + return moment().utcOffset(); +} + +export function getUtcOffsetForTimeZone(timezone) { + return moment.tz(timezone).utcOffset(); } export function isTimezoneEnabled(state) { diff --git a/package-lock.json b/package-lock.json index 47934b919..ecb6c9a1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10545,8 +10545,15 @@ "moment": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", - "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==", - "dev": true + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + }, + "moment-timezone": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", + "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", + "requires": { + "moment": ">= 2.9.0" + } }, "morgan": { "version": "1.9.0", diff --git a/package.json b/package.json index 8c98bfdb2..f5f511838 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "jsc-android": "216113.0.3", "mattermost-redux": "github:mattermost/mattermost-redux#28be3683bff6ae8c563883bb0ff04d7bd9ea31ee", "mime-db": "1.33.0", + "moment-timezone": "0.5.21", "prop-types": "15.6.1", "react": "16.3.2", "react-intl": "2.4.0", From fd99e0489c9356346171daf09de68de6af8764d9 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Tue, 4 Sep 2018 22:52:45 +0800 Subject: [PATCH 4/7] [MM-11928] Fix styling of show more/less on message attachment similar to "long post" (#2070) * fix styling of show more/less on message attachment similar to "long post" * change top margin from 0 to 10 of "Show Less" button --- .../message_attachments/message_attachment.js | 36 +- app/components/post_body/post_body.js | 115 +-- .../show_more_button.test.js.snap | 893 ++++++++++++++++++ app/components/show_more_button/index.js | 16 + .../show_more_button/show_more_button.js | 142 +++ .../show_more_button/show_more_button.test.js | 64 ++ 6 files changed, 1133 insertions(+), 133 deletions(-) create mode 100644 app/components/show_more_button/__snapshots__/show_more_button.test.js.snap create mode 100644 app/components/show_more_button/index.js create mode 100644 app/components/show_more_button/show_more_button.js create mode 100644 app/components/show_more_button/show_more_button.test.js diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js index d057df232..2ccd9f1a0 100644 --- a/app/components/message_attachments/message_attachment.js +++ b/app/components/message_attachments/message_attachment.js @@ -13,9 +13,10 @@ import { View, } from 'react-native'; -import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; import ProgressiveImage from 'app/components/progressive_image'; +import ShowMoreButton from 'app/components/show_more_button'; + import CustomPropTypes from 'app/constants/custom_prop_types'; import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; @@ -103,10 +104,10 @@ export default class MessageAttachment extends PureComponent { const {height} = event.nativeEvent.layout; const {height: deviceHeight} = Dimensions.get('window'); - if (height >= (deviceHeight * 1.2)) { + if (height >= (deviceHeight * 0.6)) { this.setState({ isLongText: true, - maxHeight: (deviceHeight * 0.6), + maxHeight: (deviceHeight * 0.4), }); } }; @@ -385,23 +386,6 @@ export default class MessageAttachment extends PureComponent { let text; if (attachment.text) { - let moreLessLocale = {id: 'post_attachment.collapse', defaultMessage: 'Show less...'}; - if (collapsed) { - moreLessLocale = {id: 'post_attachment.more', defaultMessage: 'Show more...'}; - } - - let moreLess; - if (isLongText) { - moreLess = ( - - ); - } - text = ( - {moreLess} + {isLongText && + + } ); } @@ -523,11 +512,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { height: 45, width: 45, }, - moreLess: { - color: theme.linkColor, - fontSize: 12, - marginTop: 5, - }, headingContainer: { alignSelf: 'stretch', flexDirection: 'row', diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index f5ef0166b..5208c955e 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -6,14 +6,12 @@ import PropTypes from 'prop-types'; import { Dimensions, Platform, - Text, TouchableHighlight, TouchableOpacity, View, } from 'react-native'; import {intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; -import LinearGradient from 'react-native-linear-gradient'; import {Posts} from 'mattermost-redux/constants'; @@ -21,6 +19,7 @@ import CombinedSystemMessage from 'app/components/combined_system_message'; import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; import OptionsContext from 'app/components/options_context'; +import ShowMoreButton from 'app/components/show_more_button'; import {emptyFunction} from 'app/utils/general'; import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; @@ -336,64 +335,12 @@ export default class PostBody extends PureComponent { ); }; - renderShowMoreOption = (style) => { - const {highlight, theme} = this.props; - const {isLongPost} = this.state; - - if (!isLongPost) { - return null; - } - - const gradientColors = []; - if (highlight) { - gradientColors.push( - changeOpacity(theme.mentionHighlightBg, 0), - changeOpacity(theme.mentionHighlightBg, 0.15), - changeOpacity(theme.mentionHighlightBg, 0.5), - ); - } else { - gradientColors.push( - changeOpacity(theme.centerChannelBg, 0), - changeOpacity(theme.centerChannelBg, 0.75), - theme.centerChannelBg, - ); - } - - return ( - - - - - - - - {'+'} - - - - - - - - ); - }; - render() { const {formatMessage} = this.context.intl; const { hasBeenDeleted, hasBeenEdited, + highlight, isFailed, isPending, isPostAddChannelMember, @@ -496,7 +443,12 @@ export default class PostBody extends PureComponent { > {messageComponent} - {this.renderShowMoreOption(style)} + {isLongPost && + + } {this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)} {this.renderFileAttachments()} @@ -562,56 +514,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { systemMessage: { opacity: 0.6, }, - showMoreGradient: { - flex: 1, - height: 50, - position: 'absolute', - top: -50, - width: '100%', - }, - showMoreContainer: { - alignItems: 'center', - justifyContent: 'center', - flex: 1, - flexDirection: 'row', - position: 'relative', - top: -7.5, - }, - showMoreDividerLeft: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - flex: 1, - height: 1, - marginRight: 10, - }, - showMoreDividerRight: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - flex: 1, - height: 1, - marginLeft: 10, - }, - showMoreButtonContainer: { - backgroundColor: theme.centerChannelBg, - borderColor: changeOpacity(theme.centerChannelColor, 0.2), - borderRadius: 4, - borderWidth: 1, - height: 37, - paddingHorizontal: 10, - }, - showMoreButton: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - }, - showMorePlusSign: { - color: theme.linkColor, - fontSize: 16, - fontWeight: '600', - marginRight: 8, - }, - showMoreText: { - color: theme.linkColor, - fontSize: 13, - fontWeight: '600', - }, }; }); diff --git a/app/components/show_more_button/__snapshots__/show_more_button.test.js.snap b/app/components/show_more_button/__snapshots__/show_more_button.test.js.snap new file mode 100644 index 000000000..283dae457 --- /dev/null +++ b/app/components/show_more_button/__snapshots__/show_more_button.test.js.snap @@ -0,0 +1,893 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShowMoreButton should match, button snapshot 1`] = ` + + + + + + + +`; + +exports[`ShowMoreButton should match, button snapshot 2`] = ` + + + - + + + +`; + +exports[`ShowMoreButton should match, full 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 { + "children": Array [ + , + + + + + + + + + + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "colors": Array [ + "rgba(47,62,78,0)", + "rgba(47,62,78,0.75)", + "#2f3e4e", + ], + "locations": Array [ + 0, + 0.7, + 1, + ], + "style": Object { + "flex": 1, + "height": 50, + "position": "absolute", + "top": -50, + "width": "100%", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + + + + + + + , + , + ], + "style": Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "center", + "position": "relative", + "top": 10, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "rgba(221,221,221,0.2)", + "flex": 1, + "height": 1, + "marginRight": 10, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "activeOpacity": 0.2, + "children": + + + + + + , + "onPress": [MockFunction], + "style": Object { + "backgroundColor": "#2f3e4e", + "borderColor": "rgba(221,221,221,0.2)", + "borderRadius": 4, + "borderWidth": 1, + "height": 37, + "paddingHorizontal": 10, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + + , + , + ], + "style": Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "accessible": true, + "allowFontScaling": true, + "children": "+", + "ellipsizeMode": "tail", + "style": Object { + "color": undefined, + "fontSize": 16, + "fontWeight": "600", + "marginRight": 8, + }, + }, + "ref": null, + "rendered": "+", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Show More", + "id": "post_info.message.show_more", + "style": Object { + "color": undefined, + "fontSize": 13, + "fontWeight": "600", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "rgba(221,221,221,0.2)", + "flex": 1, + "height": 1, + "marginLeft": 10, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + + + + + + + + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "colors": Array [ + "rgba(47,62,78,0)", + "rgba(47,62,78,0.75)", + "#2f3e4e", + ], + "locations": Array [ + 0, + 0.7, + 1, + ], + "style": Object { + "flex": 1, + "height": 50, + "position": "absolute", + "top": -50, + "width": "100%", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + + + + + + + , + , + ], + "style": Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "center", + "position": "relative", + "top": 10, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "rgba(221,221,221,0.2)", + "flex": 1, + "height": 1, + "marginRight": 10, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "activeOpacity": 0.2, + "children": + + + + + + , + "onPress": [MockFunction], + "style": Object { + "backgroundColor": "#2f3e4e", + "borderColor": "rgba(221,221,221,0.2)", + "borderRadius": 4, + "borderWidth": 1, + "height": 37, + "paddingHorizontal": 10, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + + , + , + ], + "style": Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "accessible": true, + "allowFontScaling": true, + "children": "+", + "ellipsizeMode": "tail", + "style": Object { + "color": undefined, + "fontSize": 16, + "fontWeight": "600", + "marginRight": 8, + }, + }, + "ref": null, + "rendered": "+", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "defaultMessage": "Show More", + "id": "post_info.message.show_more", + "style": Object { + "color": undefined, + "fontSize": 13, + "fontWeight": "600", + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "rgba(221,221,221,0.2)", + "flex": 1, + "height": 1, + "marginLeft": 10, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + }, +} +`; diff --git a/app/components/show_more_button/index.js b/app/components/show_more_button/index.js new file mode 100644 index 000000000..0002f53a2 --- /dev/null +++ b/app/components/show_more_button/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; + +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import ShowMoreButton from './show_more_button'; + +function mapStateToProps(state) { + return { + theme: getTheme(state), + }; +} + +export default connect(mapStateToProps)(ShowMoreButton); diff --git a/app/components/show_more_button/show_more_button.js b/app/components/show_more_button/show_more_button.js new file mode 100644 index 000000000..48e783378 --- /dev/null +++ b/app/components/show_more_button/show_more_button.js @@ -0,0 +1,142 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Text, TouchableOpacity, View} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +import FormattedText from 'app/components/formatted_text'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class ShowMoreButton extends PureComponent { + static propTypes = { + highlight: PropTypes.bool, + onPress: PropTypes.func.isRequired, + showMore: PropTypes.bool.isRequired, + theme: PropTypes.object.isRequired, + }; + + static defaultProps = { + showMore: true, + }; + + renderButton(showMore, style) { + let sign = '+'; + let textId = 'post_info.message.show_more'; + let textMessage = 'Show More'; + if (!showMore) { + sign = '-'; + textId = 'post_info.message.show_less'; + textMessage = 'Show Less'; + } + + return ( + + {sign} + + + ); + } + + render() { + const {highlight, showMore, theme} = this.props; + const style = getStyleSheet(theme, showMore); + + let gradientColors = [ + changeOpacity(theme.centerChannelBg, 0), + changeOpacity(theme.centerChannelBg, 0.75), + theme.centerChannelBg, + ]; + if (highlight) { + gradientColors = [ + changeOpacity(theme.mentionHighlightBg, 0), + changeOpacity(theme.mentionHighlightBg, 0.15), + changeOpacity(theme.mentionHighlightBg, 0.5), + ]; + } + + return ( + + {showMore && + + } + + + + {this.renderButton(showMore, style)} + + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme, showMore) => { + return { + gradient: { + flex: 1, + height: 50, + position: 'absolute', + top: -50, + width: '100%', + }, + container: { + alignItems: 'center', + justifyContent: 'center', + flex: 1, + flexDirection: 'row', + position: 'relative', + top: showMore ? -7.5 : 10, + }, + dividerLeft: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + flex: 1, + height: 1, + marginRight: 10, + }, + buttonContainer: { + backgroundColor: theme.centerChannelBg, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderRadius: 4, + borderWidth: 1, + height: 37, + paddingHorizontal: 10, + }, + button: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + }, + sign: { + color: theme.linkColor, + fontSize: 16, + fontWeight: '600', + marginRight: 8, + }, + text: { + color: theme.linkColor, + fontSize: 13, + fontWeight: '600', + }, + dividerRight: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + flex: 1, + height: 1, + marginLeft: 10, + }, + }; +}); diff --git a/app/components/show_more_button/show_more_button.test.js b/app/components/show_more_button/show_more_button.test.js new file mode 100644 index 000000000..7c63aa8cd --- /dev/null +++ b/app/components/show_more_button/show_more_button.test.js @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {TouchableOpacity} from 'react-native'; +import {configure, shallow} from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +configure({adapter: new Adapter()}); + +import LinearGradient from 'react-native-linear-gradient'; + +import ShowMoreButton from './show_more_button'; + +describe('ShowMoreButton', () => { + const baseProps = { + highlight: false, + onPress: jest.fn(), + showMore: true, + theme: { + centerChannelBg: '#2f3e4e', + centerChannelColor: '#dddddd', + }, + }; + + test('should match, full snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match, button snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper.instance().renderButton(true, {button: {}, sign: {}, text: {}})).toMatchSnapshot(); + expect(wrapper.instance().renderButton(false, {button: {}, sign: {}, text: {}})).toMatchSnapshot(); + }); + + test('should LinearGradient exists', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(LinearGradient).exists()).toBe(true); + wrapper.setProps({showMore: false}); + expect(wrapper.find(LinearGradient).exists()).toBe(false); + }); + + test('should call props.onPress on press of TouchableOpacity', () => { + const onPress = jest.fn(); + const wrapper = shallow( + + ); + + wrapper.find(TouchableOpacity).props().onPress(); + expect(onPress).toHaveBeenCalledTimes(1); + }); +}); From 82dc62dfc69af2c7fd5a47f4d7a916d857b6cf19 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 4 Sep 2018 11:53:15 -0300 Subject: [PATCH 5/7] Fix Posts cutoff (#2071) * Remove PixelRatio from emoji * Fix removeSubclippedViews in emoji picker * Add system emojis to the store handler * Pass element to whyDidYouUpdate util * Fix post avoidable re-renders * Fix emoji on Android * Feedback review --- app/components/emoji/emoji.js | 54 +++++-------------- app/components/emoji_picker/emoji_picker.js | 2 + .../emoji_picker/emoji_picker_row.js | 3 +- .../options_context.android.js | 12 ++--- .../options_context/options_context.ios.js | 22 +++----- app/components/post/index.js | 14 ++--- app/components/post/post.js | 42 ++------------- app/components/post_body/post_body.js | 6 ++- app/components/post_header/post_header.js | 4 +- app/components/post_list/post_list.js | 8 +-- app/components/post_list/with_layout.js | 3 +- app/screens/entry/entry.js | 10 +++- app/screens/entry/index.js | 7 +-- app/utils/why_did_you_update.js | 6 +-- 14 files changed, 69 insertions(+), 124 deletions(-) diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js index c2112dbe2..dcb6b2126 100644 --- a/app/components/emoji/emoji.js +++ b/app/components/emoji/emoji.js @@ -14,13 +14,6 @@ import { import CustomPropTypes from 'app/constants/custom_prop_types'; import ImageCacheManager from 'app/utils/image_cache_manager'; -const scaleEmojiBasedOnDevice = (size) => { - if (Platform.OS === 'ios') { - return size * 1.1; // slightly larger emojis look better on ios - } - return size * PixelRatio.get(); -}; - export default class Emoji extends React.PureComponent { static propTypes = { @@ -60,30 +53,28 @@ export default class Emoji extends React.PureComponent { this.state = { imageUrl: null, - originalWidth: 0, - originalHeight: 0, }; } componentWillMount() { + const {displayTextOnly, imageUrl} = this.props; this.mounted = true; - if (!this.props.displayTextOnly && this.props.imageUrl) { - ImageCacheManager.cache(this.props.imageUrl, this.props.imageUrl, this.updateImageHeight); + if (!displayTextOnly && imageUrl) { + ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl); } } componentWillReceiveProps(nextProps) { - if (nextProps.emojiName !== this.props.emojiName) { + const {displayTextOnly, emojiName, imageUrl} = nextProps; + if (emojiName !== this.props.emojiName) { this.setState({ imageUrl: null, - originalWidth: 0, - originalHeight: 0, }); } - if (!nextProps.displayTextOnly && nextProps.imageUrl && - nextProps.imageUrl !== this.props.imageUrl) { - ImageCacheManager.cache(nextProps.imageUrl, nextProps.imageUrl, this.updateImageHeight); + if (!displayTextOnly && imageUrl && + imageUrl !== this.props.imageUrl) { + ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl); } } @@ -91,21 +82,15 @@ export default class Emoji extends React.PureComponent { this.mounted = false; } - updateImageHeight = (imageUrl) => { + setImageUrl = (imageUrl) => { let prefix = ''; if (Platform.OS === 'android') { prefix = 'file://'; } const uri = `${prefix}${imageUrl}`; - Image.getSize(uri, (originalWidth, originalHeight) => { - if (this.mounted) { - this.setState({ - imageUrl: uri, - originalWidth, - originalHeight, - }); - } + this.setState({ + imageUrl: uri, }); }; @@ -122,26 +107,15 @@ export default class Emoji extends React.PureComponent { if (!size && textStyle) { const flatten = StyleSheet.flatten(textStyle); fontSize = flatten.fontSize; - size = scaleEmojiBasedOnDevice(fontSize); + size = fontSize * (Platform.OS === 'android' ? PixelRatio.get() : 1); } if (displayTextOnly) { return {literal}; } - let width = size; - let height = size; - let {originalHeight, originalWidth} = this.state; - originalHeight = scaleEmojiBasedOnDevice(originalHeight); - originalWidth = scaleEmojiBasedOnDevice(originalWidth); - if (originalHeight && originalWidth) { - if (originalWidth > originalHeight) { - height = (size * originalHeight) / originalWidth; - } else if (originalWidth < originalHeight) { - // This may cause text to reflow, but its impossible to add a horizontal margin - width = (size * originalWidth) / originalHeight; - } - } + const width = size; + const height = size; let marginTop = 0; if (textStyle) { diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js index 10a391c0a..509cf781d 100644 --- a/app/components/emoji_picker/emoji_picker.js +++ b/app/components/emoji_picker/emoji_picker.js @@ -407,6 +407,7 @@ export default class EmojiPicker extends PureComponent { renderItem={this.flatListRenderItem} pageSize={10} initialListSize={10} + removeClippedSubviews={true} /> ); } else { @@ -558,6 +559,7 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => { borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2), borderRightWidth: 1, borderRightColor: changeOpacity(theme.centerChannelColor, 0.2), + overflow: 'hidden', }, listView: { backgroundColor: theme.centerChannelBg, diff --git a/app/components/emoji_picker/emoji_picker_row.js b/app/components/emoji_picker/emoji_picker_row.js index 0c1937bab..4f69eccac 100644 --- a/app/components/emoji_picker/emoji_picker_row.js +++ b/app/components/emoji_picker/emoji_picker_row.js @@ -64,7 +64,7 @@ export default class EmojiPickerRow extends Component { /> ); - } + }; render() { const {emojiGutter, items} = this.props; @@ -86,6 +86,7 @@ const styles = StyleSheet.create({ emoji: { alignItems: 'center', justifyContent: 'center', + overflow: 'hidden', }, emojiLeft: { marginLeft: 0, diff --git a/app/components/options_context/options_context.android.js b/app/components/options_context/options_context.android.js index 2f1c63a93..f0249d87c 100644 --- a/app/components/options_context/options_context.android.js +++ b/app/components/options_context/options_context.android.js @@ -8,7 +8,7 @@ import RNBottomSheet from 'react-native-bottom-sheet'; export default class OptionsContext extends PureComponent { static propTypes = { - actions: PropTypes.array, + getPostActions: PropTypes.func, cancelText: PropTypes.string, children: PropTypes.node.isRequired, onPress: PropTypes.func.isRequired, @@ -16,13 +16,13 @@ export default class OptionsContext extends PureComponent { }; static defaultProps = { - actions: [], + getPostActions: () => [], cancelText: 'Cancel', }; show = (additionalAction) => { - const {actions, cancelText} = this.props; - const nextActions = [...actions]; + const {getPostActions, cancelText} = this.props; + const nextActions = getPostActions(); if (additionalAction && !additionalAction.nativeEvent && additionalAction.text) { const copyPostIndex = nextActions.findIndex((action) => action.copyPost); nextActions.splice(copyPostIndex + 1, 0, additionalAction); @@ -45,11 +45,11 @@ export default class OptionsContext extends PureComponent { }; handleHideUnderlay = () => { - this.props.toggleSelected(false, this.props.actions.length > 0); + this.props.toggleSelected(false, this.props.getPostActions().length > 0); }; handleShowUnderlay = () => { - this.props.toggleSelected(true, this.props.actions.length > 0); + this.props.toggleSelected(true, this.props.getPostActions().length > 0); }; render() { diff --git a/app/components/options_context/options_context.ios.js b/app/components/options_context/options_context.ios.js index 9c3fe3896..0e89b3f2d 100644 --- a/app/components/options_context/options_context.ios.js +++ b/app/components/options_context/options_context.ios.js @@ -8,14 +8,14 @@ import ToolTip from 'app/components/tooltip'; export default class OptionsContext extends PureComponent { static propTypes = { - actions: PropTypes.array, + getPostActions: PropTypes.func, children: PropTypes.node.isRequired, onPress: PropTypes.func.isRequired, toggleSelected: PropTypes.func.isRequired, }; static defaultProps = { - actions: [], + getPostActions: () => [], additionalActions: [], }; @@ -23,16 +23,10 @@ export default class OptionsContext extends PureComponent { super(props); this.state = { - actions: props.actions, + actions: props.getPostActions(), }; } - componentWillReceiveProps(nextProps) { - if (this.props.actions !== nextProps.actions) { - this.setState({actions: nextProps.actions}); - } - } - handleHideUnderlay = () => { if (!this.isShowing) { this.props.toggleSelected(false, false); @@ -45,11 +39,11 @@ export default class OptionsContext extends PureComponent { handleHide = () => { this.isShowing = false; - this.props.toggleSelected(false, this.props.actions.length > 0); + this.props.toggleSelected(false, this.props.getPostActions().length > 0); }; handleShow = () => { - this.isShowing = this.props.actions.length > 0; + this.isShowing = this.props.getPostActions().length > 0; this.props.toggleSelected(true, this.isShowing); }; @@ -59,12 +53,12 @@ export default class OptionsContext extends PureComponent { } this.setState({ - actions: this.props.actions, + actions: this.props.getPostActions(), }); }; show = (additionalAction) => { - const nextActions = [...this.props.actions]; + const nextActions = this.props.getPostActions(); if (additionalAction && additionalAction.text && !additionalAction.nativeEvent) { const copyPostIndex = nextActions.findIndex((action) => action.copyPost); nextActions.splice(copyPostIndex + 1, 0, additionalAction); @@ -80,7 +74,7 @@ export default class OptionsContext extends PureComponent { }; handlePress = () => { - this.props.toggleSelected(false, this.props.actions.length > 0); + this.props.toggleSelected(false, this.props.getPostActions().length > 0); this.props.onPress(); }; diff --git a/app/components/post/index.js b/app/components/post/index.js index d719c3bd5..97d74b1cf 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -5,6 +5,7 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts'; +import {General, Posts} from 'mattermost-redux/constants'; import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels'; import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; @@ -16,8 +17,6 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel'; import {addReaction} from 'app/actions/views/emoji'; -import {getDimensions} from 'app/selectors/device'; -import {Posts} from 'mattermost-redux/constants'; import Post from './post'; @@ -78,33 +77,36 @@ function makeMapStateToProps() { } } } - const {deviceWidth} = getDimensions(state); const isAdmin = checkIsAdmin(roles); const isSystemAdmin = checkIsSystemAdmin(roles); let canDelete = false; let canEdit = false; + let canEditUntil = -1; if (post) { canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin); canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); + if (canEdit && license.IsLicensed === 'true' && + (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1')) + ) { + canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000); + } } return { channelIsReadOnly: isCurrentChannelReadOnly(state), - config, canDelete, canEdit, + canEditUntil, currentTeamUrl: getCurrentTeamUrl(state), currentUserId, - deviceWidth, post, isFirstReply, isLastReply, consecutivePost: isConsecutivePost(state, ownProps), hasComments: getCommentCountForPost(state, {post}) > 0, commentedOnPost, - license, theme: getTheme(state), isFlagged: isPostFlagged(post.id, myPreferences), }; diff --git a/app/components/post/post.js b/app/components/post/post.js index 2dbfa03bc..37bea8265 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -25,9 +25,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {getToolTipVisible} from 'app/utils/tooltip'; import {Posts} from 'mattermost-redux/constants'; -import DelayedAction from 'mattermost-redux/utils/delayed_action'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {editDisable, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; import Config from 'assets/config'; @@ -41,10 +40,8 @@ export default class Post extends PureComponent { removePost: PropTypes.func.isRequired, }).isRequired, channelIsReadOnly: PropTypes.bool, - config: PropTypes.object.isRequired, currentTeamUrl: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, - deviceWidth: PropTypes.number.isRequired, highlight: PropTypes.bool, style: ViewPropTypes.style, post: PropTypes.object, @@ -56,10 +53,10 @@ export default class Post extends PureComponent { hasComments: PropTypes.bool, isSearchResult: PropTypes.bool, commentedOnPost: PropTypes.object, - license: PropTypes.object.isRequired, managedConfig: PropTypes.object.isRequired, navigator: PropTypes.object, canEdit: PropTypes.bool.isRequired, + canEditUntil: PropTypes.number.isRequired, canDelete: PropTypes.bool.isRequired, onPermalinkPress: PropTypes.func, shouldRenderReplyButton: PropTypes.bool, @@ -83,37 +80,6 @@ export default class Post extends PureComponent { intl: intlShape.isRequired, }; - constructor(props) { - super(props); - - const {config, license, currentUserId, post} = props; - this.editDisableAction = new DelayedAction(this.handleEditDisable); - if (post) { - editDisable(config, license, currentUserId, post, this.editDisableAction); - } - this.state = { - canEdit: this.props.canEdit, - }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.config !== this.props.config || - nextProps.license !== this.props.license || - nextProps.currentUserId !== this.props.currentUserId || - nextProps.post !== this.props.post) { - const {config, license, currentUserId, post} = nextProps; - - editDisable(config, license, currentUserId, post, this.editDisableAction); - this.setState({ - canEdit: nextProps.canEdit, - }); - } - } - - componentWillUnmount() { - this.editDisableAction.cancel(); - } - goToUserProfile = () => { const {intl} = this.context; const {navigator, post, theme} = this.props; @@ -165,7 +131,6 @@ export default class Post extends PureComponent { text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), style: 'destructive', onPress: () => { - this.editDisableAction.cancel(); actions.deletePost(post); if (post.user_id === currentUserId) { actions.removePost(post); @@ -479,7 +444,8 @@ export default class Post extends PureComponent { Date.now())) { actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit}); } @@ -435,7 +437,7 @@ export default class PostBody extends PureComponent { body = ( + {this.getDisplayName(style)} @@ -253,7 +253,7 @@ export default class PostHeader extends PureComponent { {this.renderCommentedOnMessage(style)} } - + ); } } diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index af86fad6d..c933abc5b 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -234,9 +234,11 @@ export default class PostList extends PureComponent { nextConfig = await mattermostManaged.getLocalConfig(); } - this.setState({ - managedConfig: nextConfig, - }); + if (Object.keys(nextConfig).length) { + this.setState({ + managedConfig: nextConfig, + }); + } }; keyExtractor = (item) => { diff --git a/app/components/post_list/with_layout.js b/app/components/post_list/with_layout.js index dcef92225..5ed73e34a 100644 --- a/app/components/post_list/with_layout.js +++ b/app/components/post_list/with_layout.js @@ -28,9 +28,10 @@ function withLayout(WrappedComponent) { }; render() { + const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars return ( - + ); } diff --git a/app/screens/entry/entry.js b/app/screens/entry/entry.js index 640396b19..8f61e1884 100644 --- a/app/screens/entry/entry.js +++ b/app/screens/entry/entry.js @@ -11,6 +11,7 @@ import { import DeviceInfo from 'react-native-device-info'; +import {setSystemEmojis} from 'mattermost-redux/actions/emojis'; import {Client4} from 'mattermost-redux/client'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -56,15 +57,14 @@ const lazyLoadReplyPushNotifications = () => { */ export default class Entry extends PureComponent { static propTypes = { - config: PropTypes.object, theme: PropTypes.object, navigator: PropTypes.object, isLandscape: PropTypes.bool, - hydrationComplete: PropTypes.bool, enableTimezone: PropTypes.bool, deviceTimezone: PropTypes.string, initializeModules: PropTypes.func.isRequired, actions: PropTypes.shape({ + autoUpdateTimezone: PropTypes.func.isRequired, setDeviceToken: PropTypes.func.isRequired, }).isRequired, }; @@ -132,6 +132,7 @@ export default class Entry extends PureComponent { this.setAppCredentials(); this.setStartupThemes(); this.handleNotification(); + this.loadSystemEmojis(); if (Platform.OS === 'android') { this.launchForAndroid(); @@ -226,6 +227,11 @@ export default class Entry extends PureComponent { } }; + loadSystemEmojis = () => { + const EmojiIndicesByAlias = require('app/utils/emojis').EmojiIndicesByAlias; + setSystemEmojis(EmojiIndicesByAlias); + }; + renderLogin = () => { const SelectServer = lazyLoadSelectServer(); const props = { diff --git a/app/screens/entry/index.js b/app/screens/entry/index.js index 5048a5537..dab3808b8 100644 --- a/app/screens/entry/index.js +++ b/app/screens/entry/index.js @@ -6,7 +6,6 @@ import {connect} from 'react-redux'; import {setDeviceToken} from 'mattermost-redux/actions/general'; import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {isLandscape} from 'app/selectors/device'; import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone'; @@ -16,16 +15,12 @@ const lazyLoadEntry = () => { }; function mapStateToProps(state) { - const config = getConfig(state); - const enableTimezone = isTimezoneEnabled(state); const deviceTimezone = getDeviceTimezone(); return { - config, theme: getTheme(state), isLandscape: isLandscape(state), - hydrationComplete: state.views.root.hydrationComplete, enableTimezone, deviceTimezone, }; @@ -34,8 +29,8 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - setDeviceToken, autoUpdateTimezone, + setDeviceToken, }, dispatch), }; } diff --git a/app/utils/why_did_you_update.js b/app/utils/why_did_you_update.js index 891a0aff7..973476d37 100644 --- a/app/utils/why_did_you_update.js +++ b/app/utils/why_did_you_update.js @@ -40,10 +40,10 @@ function deepDiff(o1, o2, p) { } } -function whyDidYouUpdate(prevProps, prevState) { +function whyDidYouUpdate(theClass, prevProps, prevState) { deepDiff({props: prevProps, state: prevState}, - {props: this.props, state: this.state}, - this.constructor.name); + {props: theClass.props, state: theClass.state}, + theClass.constructor.name); } export default whyDidYouUpdate; From 82cee7a84642a83943c97e477d0d6c0aed4fa6b2 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 4 Sep 2018 16:36:54 -0300 Subject: [PATCH 6/7] Bump iOS build number to 136 (#2073) --- 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 363e2b0e3..a60eb2048 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -2427,7 +2427,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2476,7 +2476,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index c72ccb751..a5f221f54 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 135 + 136 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index f28831374..72fcd2680 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -23,7 +23,7 @@ CFBundleShortVersionString 1.12.0 CFBundleVersion - 135 + 136 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index 9e5b4b303..1be755e85 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 135 + 136 From 65fce2a6802054c8f7e85b81a43b0d95dca4a08b Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 4 Sep 2018 16:38:22 -0300 Subject: [PATCH 7/7] Bump Android build number to 136 (#2074) --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e1117e6b..886fb4c15 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -113,7 +113,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion 21 targetSdkVersion 26 - versionCode 135 + versionCode 136 versionName "1.12.0" multiDexEnabled = true ndk {