Various fixes (#3078)

* MM-17588 Remove navigation component from stack

* MM-175986 Fix Clock Display Settings on iOS

* Fix markdown and team icon currentServerUrl

* Fix closing permalink logs out the user

* Fix file attachment document ref

* Fix applyTheme when changing a theme in the app

* Feedback review

* remove / when fetching the image on the markdown table on relative paths
This commit is contained in:
Elias Nahum
2019-08-08 10:28:07 -04:00
committed by Saturnino Abril
parent 24bd57ad3f
commit 7b75868101
22 changed files with 129 additions and 144 deletions

View File

@@ -360,24 +360,22 @@ export function dismissOverlay(componentId) {
};
}
export function applyTheme() {
export function applyTheme(componentId) {
return (dispatch, getState) => {
const theme = getTheme(getState());
EphemeralStore.getNavigationComponentIds().forEach((componentId) => {
Navigation.mergeOptions(componentId, {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
Navigation.mergeOptions(componentId, {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
},
});
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
});
};
}

View File

@@ -16,4 +16,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(null, mapDispatchToProps)(FileAttachmentDocument);
export default connect(null, mapDispatchToProps, null, {forwardRef: true})(FileAttachmentDocument);

View File

@@ -18,6 +18,7 @@ import {
import FormattedText from 'app/components/formatted_text';
import ProgressiveImage from 'app/components/progressive_image';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {getCurrentServerUrl} from 'app/init/credentials';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import ImageCacheManager from 'app/utils/image_cache_manager';
@@ -42,7 +43,7 @@ export default class MarkdownImage extends React.Component {
imagesMetadata: PropTypes.object,
linkDestination: PropTypes.string,
isReplyPost: PropTypes.bool,
serverURL: PropTypes.string.isRequired,
serverURL: PropTypes.string,
source: PropTypes.string.isRequired,
errorTextStyle: CustomPropTypes.Style,
};
@@ -95,11 +96,15 @@ export default class MarkdownImage extends React.Component {
this.mounted = false;
}
getSource = () => {
getSource = async () => {
let source = this.props.source;
let serverUrl = this.props.serverURL;
if (!serverUrl) {
serverUrl = await getCurrentServerUrl();
}
if (source.startsWith('/')) {
source = this.props.serverURL + '/' + source;
source = serverUrl + source;
}
return source;

View File

@@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {DeepLinkTypes} from 'app/constants';
import {getCurrentServerUrl} from 'app/init/credentials';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {preventDoubleTap} from 'app/utils/tap';
@@ -24,7 +25,7 @@ export default class MarkdownLink extends PureComponent {
children: CustomPropTypes.Children.isRequired,
href: PropTypes.string.isRequired,
onPermalinkPress: PropTypes.func,
serverURL: PropTypes.string.isRequired,
serverURL: PropTypes.string,
siteURL: PropTypes.string.isRequired,
};
@@ -38,7 +39,7 @@ export default class MarkdownLink extends PureComponent {
intl: intlShape.isRequired,
};
handlePress = preventDoubleTap(() => {
handlePress = preventDoubleTap(async () => {
const {href, onPermalinkPress, serverURL, siteURL} = this.props;
const url = normalizeProtocol(href);
@@ -46,7 +47,12 @@ export default class MarkdownLink extends PureComponent {
return;
}
const match = matchDeepLink(url, serverURL, siteURL);
let serverUrl = serverURL;
if (!serverUrl) {
serverUrl = await getCurrentServerUrl();
}
const match = matchDeepLink(url, serverUrl, siteURL);
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);

View File

@@ -7,6 +7,7 @@ import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {getCurrentServerUrl} from 'app/init/credentials';
import {preventDoubleTap} from 'app/utils/tap';
export default class MarkdownTableImage extends React.PureComponent {
@@ -17,7 +18,7 @@ export default class MarkdownTableImage extends React.PureComponent {
children: PropTypes.node.isRequired,
source: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style.isRequired,
serverURL: PropTypes.string.isRequired,
serverURL: PropTypes.string,
theme: PropTypes.object.isRequired,
};
@@ -40,11 +41,16 @@ export default class MarkdownTableImage extends React.PureComponent {
actions.goToScreen(screen, title, passProps);
});
getImageSource = () => {
getImageSource = async () => {
let source = this.props.source;
let serverUrl = this.props.serverURL;
if (!serverUrl) {
serverUrl = await getCurrentServerUrl();
}
if (source.startsWith('/')) {
source = `${this.props.serverURL}/${source}`;
source = serverUrl + source;
}
return source;

View File

@@ -4,14 +4,12 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModal} from 'app/actions/navigation';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getCurrentLocale} from 'app/selectors/i18n';
import {removeProtocol} from 'app/utils/url';
import TeamsList from './teams_list';
@@ -20,7 +18,6 @@ function mapStateToProps(state) {
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
hasOtherJoinableTeams: getJoinableTeamIds(state).length > 0,
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state),

View File

@@ -17,8 +17,10 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {DeviceTypes, ListTypes, ViewTypes} from 'app/constants';
import {getCurrentServerUrl} from 'app/init/credentials';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {removeProtocol} from 'app/utils/url';
import tracker from 'app/utils/time_tracker';
import telemetry from 'app/telemetry';
@@ -38,7 +40,6 @@ export default class TeamsList extends PureComponent {
}).isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
hasOtherJoinableTeams: PropTypes.bool,
teamIds: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
@@ -51,9 +52,17 @@ export default class TeamsList extends PureComponent {
constructor(props) {
super(props);
this.state = {
serverUrl: '',
};
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
getCurrentServerUrl().then((url) => {
this.setState({serverUrl: removeProtocol(url)});
});
}
selectTeam = (teamId) => {
@@ -75,13 +84,14 @@ export default class TeamsList extends PureComponent {
});
};
goToSelectTeam = preventDoubleTap(() => {
goToSelectTeam = preventDoubleTap(async () => {
const {intl} = this.context;
const {currentUrl, theme, actions} = this.props;
const {theme, actions} = this.props;
const {serverUrl} = this.state;
const screen = 'SelectTeam';
const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'});
const passProps = {
currentUrl,
currentUrl: serverUrl,
theme,
};
const options = {
@@ -117,6 +127,7 @@ export default class TeamsList extends PureComponent {
renderItem = ({item}) => {
return (
<TeamsListItem
currentUrl={this.state.serverUrl}
selectTeam={this.selectTeam}
teamId={item}
/>
@@ -155,6 +166,7 @@ export default class TeamsList extends PureComponent {
{moreAction}
</View>
<FlatList
extraData={this.state.serverUrl}
contentContainerStyle={this.listContentPadding()}
data={teamIds}
renderItem={this.renderItem}

View File

@@ -3,12 +3,9 @@
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsListItem from './teams_list_item.js';
function makeMapStateToProps() {
@@ -19,7 +16,6 @@ function makeMapStateToProps() {
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
displayName: team.display_name,
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,

View File

@@ -92,4 +92,8 @@ Navigation.events().registerAppLaunchedListener(() => {
Navigation.events().registerComponentDidAppearListener(({componentId}) => {
EphemeralStore.addNavigationComponentId(componentId);
});
Navigation.events().registerComponentDidDisappearListener(({componentId}) => {
EphemeralStore.removeNavigationComponentId(componentId);
});
});

View File

@@ -66,7 +66,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default);
Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/settings/timezone/select_timezone').default), () => require('app/screens/settings/timezone/select_timezone').default);
Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default);
Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens//settings/sidebar').default), () => require('app/screens/settings/sidebar').default);
Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens/settings/sidebar').default), () => require('app/screens/settings/sidebar').default);
Navigation.registerComponent('SSO', () => wrapper(require('app/screens/sso').default), () => require('app/screens/sso').default);
Navigation.registerComponent('Table', () => wrapper(require('app/screens/table').default), () => require('app/screens/table').default);
Navigation.registerComponent('TableImage', () => wrapper(require('app/screens/table_image').default), () => require('app/screens/table_image').default);

View File

@@ -267,41 +267,43 @@ export default class Permalink extends PureComponent {
const {formatMessage} = intl;
let focusChannelId = channelId;
const post = await actions.getPostThread(focusedPostId, false);
if (post.error && (!postIds || !postIds.length)) {
if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
this.setState({
error: formatMessage({
id: 'permalink.error.access',
defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
}),
title: formatMessage({
id: 'mobile.search.no_results',
defaultMessage: 'No Results Found',
}),
});
} else if (this.mounted) {
this.setState({error: post.error.message, retry: true});
if (focusedPostId) {
const post = await actions.getPostThread(focusedPostId, false);
if (post.error && (!postIds || !postIds.length)) {
if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
this.setState({
error: formatMessage({
id: 'permalink.error.access',
defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
}),
title: formatMessage({
id: 'mobile.search.no_results',
defaultMessage: 'No Results Found',
}),
});
} else if (this.mounted) {
this.setState({error: post.error.message, retry: true});
}
return;
}
return;
}
if (!channelId) {
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
focusChannelId = focusedPost ? focusedPost.channel_id : '';
if (focusChannelId) {
const {data: channel} = await actions.getChannel(focusChannelId);
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
if (!channelId) {
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
focusChannelId = focusedPost ? focusedPost.channel_id : '';
if (focusChannelId) {
const {data: channel} = await actions.getChannel(focusChannelId);
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
}
}
}
}
await actions.getPostsAround(focusChannelId, focusedPostId, 10);
await actions.getPostsAround(focusChannelId, focusedPostId, 10);
if (this.mounted) {
this.setState({loading: false});
if (this.mounted) {
this.setState({loading: false});
}
}
};

View File

@@ -3,6 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Navigation} from 'react-native-navigation';
import {intlShape} from 'react-intl';
import {
Platform,
@@ -19,6 +20,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class DisplaySettings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
applyTheme: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
@@ -35,6 +37,15 @@ export default class DisplaySettings extends PureComponent {
showClockDisplaySettings: false,
};
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
}
componentDidAppear() {
const {actions, componentId} = this.props;
actions.applyTheme(componentId);
}
closeClockDisplaySettings = () => {
this.setState({showClockDisplaySettings: false});
};
@@ -44,7 +55,7 @@ export default class DisplaySettings extends PureComponent {
const {intl} = this.context;
if (Platform.OS === 'ios') {
const screen = 'ClockDisplay';
const screen = 'ClockDisplaySettings';
const title = intl.formatMessage({id: 'user.settings.display.clockDisplay', defaultMessage: 'Clock Display'});
actions.goToScreen(screen, title);
return;

View File

@@ -16,6 +16,7 @@ jest.mock('react-intl');
describe('DisplaySettings', () => {
const baseProps = {
actions: {
applyTheme: jest.fn(),
goToScreen: jest.fn(),
},
theme: Preferences.THEMES.default,

View File

@@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {goToScreen} from 'app/actions/navigation';
import {applyTheme, goToScreen} from 'app/actions/navigation';
import {getAllowedThemes} from 'app/selectors/theme';
import {isThemeSwitchingEnabled} from 'app/utils/theme';
@@ -28,6 +28,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
applyTheme,
}, dispatch),
};
}

View File

@@ -9,7 +9,7 @@ import {getCurrentUrl, getConfig} from 'mattermost-redux/selectors/entities/gene
import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen, dismissModal} from 'app/actions/navigation';
import {applyTheme, goToScreen, dismissModal} from 'app/actions/navigation';
import {purgeOfflineStore} from 'app/actions/views/root';
import {removeProtocol} from 'app/utils/url';
@@ -32,6 +32,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
applyTheme,
clearErrors,
purgeOfflineStore,
goToScreen,

View File

@@ -25,6 +25,7 @@ import LocalConfig from 'assets/config';
class Settings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
applyTheme: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired,
purgeOfflineStore: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
@@ -50,6 +51,11 @@ class Settings extends PureComponent {
this.navigationEventListener = Navigation.events().bindComponent(this);
}
componentDidAppear() {
const {actions, componentId} = this.props;
actions.applyTheme(componentId);
}
navigationButtonPressed({buttonId}) {
if (buttonId === 'close-settings') {
this.props.actions.dismissModal();

View File

@@ -11,7 +11,6 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getAllowedThemes, getCustomTheme} from 'app/selectors/theme';
import {isLandscape, isTablet} from 'app/selectors/device';
import {applyTheme} from 'app/actions/navigation';
import Theme from './theme';
@@ -28,7 +27,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators({
savePreferences,
applyTheme,
}, dispatch),
});

View File

@@ -26,7 +26,6 @@ export default class Theme extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
savePreferences: PropTypes.func.isRequired,
applyTheme: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
allowedThemes: PropTypes.arrayOf(PropTypes.object),
@@ -66,7 +65,7 @@ export default class Theme extends React.PureComponent {
userId,
teamId,
allowedThemes,
actions: {savePreferences, applyTheme},
actions: {savePreferences},
} = this.props;
const {customTheme} = this.state;
const selectedTheme = allowedThemes.concat(customTheme).find((theme) => theme.key === key);
@@ -77,8 +76,7 @@ export default class Theme extends React.PureComponent {
name: teamId,
value: JSON.stringify(selectedTheme),
}]);
applyTheme();
}
};
renderAllowedThemeTiles = () => {
const {theme, allowedThemes, isLandscape, isTablet} = this.props;

View File

@@ -3,29 +3,14 @@
import React from 'react';
import {shallow} from 'enzyme';
import {Navigation} from 'react-native-navigation';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import Preferences from 'mattermost-redux/constants/preferences';
import {applyTheme} from 'app/actions/navigation';
import EphemeralStore from 'app/store/ephemeral_store';
import Theme from './theme';
import ThemeTile from './theme_tile';
jest.mock('react-intl');
jest.mock('react-native-navigation', () => ({
Navigation: {
mergeOptions: jest.fn(),
},
}));
const mockStore = configureMockStore([thunk]);
const allowedThemes = [
{
type: 'Mattermost',
@@ -141,7 +126,6 @@ describe('Theme', () => {
const baseProps = {
actions: {
savePreferences: jest.fn(),
applyTheme: jest.fn(),
},
allowedThemes,
isLandscape: false,
@@ -159,57 +143,4 @@ describe('Theme', () => {
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find(ThemeTile)).toHaveLength(4);
});
test('should apply new theme to all navigation components that have appeared', () => {
const componentIds = ['component-1', 'component-2', 'component-3'];
componentIds.forEach((componentId) => {
EphemeralStore.addNavigationComponentId(componentId);
});
const store = mockStore({
entities: {
preferences: {
myPreferences: {
theme: {},
},
},
teams: {
currentTeamId: 'current-team-id',
},
general: {
config: {},
},
},
});
baseProps.actions.applyTheme.mockImplementation(() => {
store.dispatch(applyTheme());
});
const wrapper = shallow(
<Theme {...baseProps}/>,
);
const theme = allowedThemes[0];
wrapper.instance().setTheme(theme.key);
expect(baseProps.actions.applyTheme).toHaveBeenCalledTimes(1);
const options = {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
};
expect(Navigation.mergeOptions.mock.calls).toEqual([
[componentIds[2], options],
[componentIds[1], options],
[componentIds[0], options],
]);
});
});

View File

@@ -19,6 +19,13 @@ class EphemeralStore {
}
this.navigationComponentIdStack.unshift(componentId);
};
removeNavigationComponentId = (componentId) => {
const index = this.navigationComponentIdStack.indexOf(componentId);
if (index >= 0) {
this.navigationComponentIdStack.splice(index, 1);
}
}
}

View File

@@ -99,7 +99,7 @@ export function getScheme(url) {
}
export function matchDeepLink(url, serverURL, siteURL) {
if (!url || !serverURL || !siteURL) {
if (!url || (!serverURL && !siteURL)) {
return null;
}

View File

@@ -33,6 +33,11 @@ jest.mock('NativeModules', () => {
END: 'END',
},
},
RNKeychainManager: {
SECURITY_LEVEL_ANY: 'ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'SOFTWARE',
SECURITY_LEVEL_SECURE_HARDWARE: 'HARDWARE',
},
};
});
jest.mock('NativeEventEmitter');