Manual cherry pick of #3298 (#3328)

This commit is contained in:
Miguel Alatzar 2019-09-25 15:53:05 -07:00 committed by GitHub
parent 5fafe376fa
commit 2eb723a6dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
215 changed files with 2703 additions and 2735 deletions

View file

@ -95,7 +95,7 @@ post-install:
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@./node_modules/.bin/patch-package --patch-dir=native_modules
@./node_modules/.bin/patch-package
start: | pre-run ## Starts the React Native packager server
$(call start_packager)

View file

@ -1,394 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import EphemeralStore from 'app/store/ephemeral_store';
export function resetToChannel(passProps = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
});
};
}
export function resetToSelectServer(allowOtherServers) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
});
};
}
export function resetToTeams(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
});
};
}
export function goToScreen(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const state = getState();
const componentId = EphemeralStore.getNavigationTopComponentId();
const theme = getTheme(state);
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function popTopScreen(screenId) {
return () => {
if (screenId) {
Navigation.pop(screenId);
} else {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.pop(componentId);
}
};
}
export function popToRoot() {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.popToRoot(componentId).catch(() => {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this
// case but we will catch the rejection here so that the
// caller doesn't have to.
});
};
}
export function showModal(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
Navigation.showModal({
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
});
};
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
return (dispatch) => {
const title = '';
const animationsEnabled = (Platform.OS === 'android').toString();
const defaultOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const mergeOptions = merge(defaultOptions, options);
dispatch(showModal(name, title, passProps, mergeOptions));
};
}
export function showSearchModal(initialValue = '') {
return (dispatch) => {
const name = 'Search';
const title = '';
const passProps = {initialValue};
const options = {
topBar: {
visible: false,
height: 0,
},
};
dispatch(showModal(name, title, passProps, options));
};
}
export function dismissModal(options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.dismissModal(componentId, options).catch(() => {
// RNN returns a promise rejection if there is no modal to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function dismissAllModals(options = {}) {
return () => {
Navigation.dismissAllModals(options).catch(() => {
// RNN returns a promise rejection if there are no modals to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function peek(name, passProps = {}, options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
preview: {
commit: false,
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
return () => {
Navigation.mergeOptions(componentId, {
topBar: {
...buttons,
},
});
};
}
export function showOverlay(name, passProps, options = {}) {
return () => {
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
Navigation.showOverlay({
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function dismissOverlay(componentId) {
return () => {
return Navigation.dismissOverlay(componentId).catch(() => {
// RNN returns a promise rejection if there is no modal with
// this componentId to dismiss. We'll do nothing in this case
// but we will catch the rejection here so that the caller
// doesn't have to.
});
};
}
export function applyTheme(componentId, skipBackButtonStyle = false) {
return (dispatch, getState) => {
const theme = getTheme(getState());
let backButton = {
color: theme.sidebarHeaderTextColor,
};
if (skipBackButtonStyle && Platform.OS === 'android') {
backButton = null;
}
Navigation.mergeOptions(componentId, {
topBar: {
backButton,
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
});
};
}

View file

@ -0,0 +1,352 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import store from 'app/store';
import EphemeralStore from 'app/store/ephemeral_store';
function getThemeFromState() {
const state = store.getState();
return getTheme(state);
}
export function resetToChannel(passProps = {}) {
const theme = getThemeFromState();
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
});
}
export function resetToSelectServer(allowOtherServers) {
const theme = getThemeFromState();
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
});
}
export function resetToTeams(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
});
}
export function goToScreen(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export function popTopScreen(screenId) {
if (screenId) {
Navigation.pop(screenId);
} else {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.pop(componentId);
}
}
export async function popToRoot() {
const componentId = EphemeralStore.getNavigationTopComponentId();
try {
await Navigation.popToRoot(componentId);
} catch (error) {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this case.
}
}
export function showModal(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
Navigation.showModal({
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
});
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
const title = '';
const animationsEnabled = (Platform.OS === 'android').toString();
const defaultOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const mergeOptions = merge(defaultOptions, options);
showModal(name, title, passProps, mergeOptions);
}
export function showSearchModal(initialValue = '') {
const name = 'Search';
const title = '';
const passProps = {initialValue};
const options = {
topBar: {
visible: false,
height: 0,
},
};
showModal(name, title, passProps, options);
}
export async function dismissModal(options = {}) {
const componentId = EphemeralStore.getNavigationTopComponentId();
try {
await Navigation.dismissModal(componentId, options);
} catch (error) {
// RNN returns a promise rejection if there is no modal to
// dismiss. We'll do nothing in this case.
}
}
export async function dismissAllModals(options = {}) {
try {
await Navigation.dismissAllModals(options);
} catch (error) {
// RNN returns a promise rejection if there are no modals to
// dismiss. We'll do nothing in this case.
}
}
export function peek(name, passProps = {}, options = {}) {
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
preview: {
commit: false,
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
const options = {
topBar: {
...buttons,
},
};
mergeNavigationOptions(componentId, options);
}
export function mergeNavigationOptions(componentId, options) {
Navigation.mergeOptions(componentId, options);
}
export function showOverlay(name, passProps, options = {}) {
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
Navigation.showOverlay({
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export async function dismissOverlay(componentId) {
try {
await Navigation.dismissOverlay(componentId);
} catch (error) {
// RNN returns a promise rejection if there is no modal with
// this componentId to dismiss. We'll do nothing in this case.
}
}

View file

@ -0,0 +1,473 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import Preferences from 'mattermost-redux/constants/preferences';
import EphemeralStore from 'app/store/ephemeral_store';
import * as NavigationActions from 'app/actions/navigation';
jest.unmock('app/actions/navigation');
jest.mock('app/store/ephemeral_store', () => ({
getNavigationTopComponentId: jest.fn(),
}));
describe('app/actions/navigation', () => {
const topComponentId = 'top-component-id';
const name = 'name';
const title = 'title';
const theme = Preferences.THEMES.default;
const passProps = {
testProp: 'prop',
};
const options = {
testOption: 'test',
};
EphemeralStore.getNavigationTopComponentId.mockReturnValue(topComponentId);
test('resetToChannel should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
};
NavigationActions.resetToChannel(passProps);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('resetToSelectServer should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const allowOtherServers = false;
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
};
NavigationActions.resetToSelectServer(allowOtherServers);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('resetToTeams should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
};
NavigationActions.resetToTeams(name, title, passProps, options);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('goToScreen should call Navigation.push', () => {
const push = jest.spyOn(Navigation, 'push');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
NavigationActions.goToScreen(name, title, passProps, options);
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
});
test('popTopScreen should call Navigation.pop', () => {
const pop = jest.spyOn(Navigation, 'pop');
NavigationActions.popTopScreen();
expect(pop).toHaveBeenCalledWith(topComponentId);
const otherComponentId = `other-${topComponentId}`;
NavigationActions.popTopScreen(otherComponentId);
expect(pop).toHaveBeenCalledWith(otherComponentId);
});
test('popToRoot should call Navigation.popToRoot', async () => {
const popToRoot = jest.spyOn(Navigation, 'popToRoot');
await NavigationActions.popToRoot();
expect(popToRoot).toHaveBeenCalledWith(topComponentId);
});
test('showModal should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const expectedLayout = {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
};
NavigationActions.showModal(name, title, passProps, options);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('showModalOverCurrentContext should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const animationsEnabled = (Platform.OS === 'android').toString();
const showModalOverCurrentContextTitle = '';
const showModalOverCurrentContextOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const showModalOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: showModalOverCurrentContextTitle,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const defaultOptions = merge(showModalOverCurrentContextOptions, options);
const expectedLayout = {
stack: {
children: [{
component: {
name,
passProps,
options: merge(showModalOptions, defaultOptions),
},
}],
},
};
NavigationActions.showModalOverCurrentContext(name, passProps, options);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('showSearchModal should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const showSearchModalName = 'Search';
const showSearchModalTitle = '';
const initialValue = 'initial-value';
const showSearchModalPassProps = {initialValue};
const showSearchModalOptions = {
topBar: {
visible: false,
height: 0,
},
};
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: showSearchModalTitle,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const expectedLayout = {
stack: {
children: [{
component: {
name: showSearchModalName,
passProps: showSearchModalPassProps,
options: merge(defaultOptions, showSearchModalOptions),
},
}],
},
};
NavigationActions.showSearchModal(initialValue);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('dismissModal should call Navigation.dismissModal', async () => {
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
await NavigationActions.dismissModal(options);
expect(dismissModal).toHaveBeenCalledWith(topComponentId, options);
});
test('dismissAllModals should call Navigation.dismissAllModals', async () => {
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
await NavigationActions.dismissAllModals(options);
expect(dismissAllModals).toHaveBeenCalledWith(options);
});
test('peek should call Navigation.push', async () => {
const push = jest.spyOn(Navigation, 'push');
const defaultOptions = {
preview: {
commit: false,
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
await NavigationActions.peek(name, passProps, options);
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
});
test('mergeNavigationOptions should call Navigation.mergeOptions', () => {
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
NavigationActions.mergeNavigationOptions(topComponentId, options);
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, options);
});
test('setButtons should call Navigation.mergeOptions', () => {
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
const buttons = {
leftButtons: ['left-button'],
rightButtons: ['right-button'],
};
const setButtonsOptions = {
topBar: {
...buttons,
},
};
NavigationActions.setButtons(topComponentId, buttons);
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, setButtonsOptions);
});
test('showOverlay should call Navigation.showOverlay', () => {
const showOverlay = jest.spyOn(Navigation, 'showOverlay');
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
NavigationActions.showOverlay(name, passProps, options);
expect(showOverlay).toHaveBeenCalledWith(expectedLayout);
});
test('dismissOverlay should call Navigation.dismissOverlay', async () => {
const dismissOverlay = jest.spyOn(Navigation, 'dismissOverlay');
await NavigationActions.dismissOverlay(topComponentId);
expect(dismissOverlay).toHaveBeenCalledWith(topComponentId);
});
});

View file

@ -10,7 +10,8 @@ import {
fetchMyChannelsAndMembers,
getChannelByNameAndTeamName,
markChannelAsRead,
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
markChannelAsViewed,
leaveChannel as serviceLeaveChannel,
selectChannel,
getChannelStats,
} from 'mattermost-redux/actions/channels';

View file

@ -20,6 +20,23 @@ jest.mock('mattermost-redux/selectors/entities/channels', () => ({
getMyChannelMember: () => ({data: {member: {}}}),
}));
jest.mock('mattermost-redux/actions/channels', () => {
const channelActions = require.requireActual('mattermost-redux/actions/channels');
return {
...channelActions,
markChannelAsRead: jest.fn(),
markChannelAsViewed: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/teams', () => {
const teamSelectors = require.requireActual('mattermost-redux/selectors/entities/teams');
return {
...teamSelectors,
getTeamByName: jest.fn(() => ({name: 'current-team-name'})),
};
});
const mockStore = configureStore([thunk]);
describe('Actions.Views.Channel', () => {

View file

@ -1,13 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`profile_picture_button should match snapshot 1`] = `
<Connect(AttachmentButton)
<AttachmentButton
blurTextBox={[MockFunction]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={
Array [
null,
]
}
maxFileCount={5}
maxFileSize={20971520}
theme={
Object {
@ -38,5 +45,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
}
}
uploadFiles={[MockFunction]}
validMimeTypes={Array []}
/>
`;

View file

@ -11,16 +11,15 @@ import {
} from 'react-native';
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import RemoveMarkdown from 'app/components/remove_markdown';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {goToScreen} from 'app/actions/navigation';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
@ -55,7 +54,6 @@ export default class AnnouncementBanner extends PureComponent {
}
handlePress = () => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'ExpandedAnnouncementBanner';
@ -64,7 +62,7 @@ export default class AnnouncementBanner extends PureComponent {
defaultMessage: 'Announcement',
});
actions.goToScreen(screen, title);
goToScreen(screen, title);
};
toggleBanner = (show = true) => {

View file

@ -12,9 +12,6 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
goToScreen: jest.fn(),
},
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,

View file

@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import AnnouncementBanner from './announcement_banner';
@ -28,12 +26,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
export default connect(mapStateToProps)(AnnouncementBanner);

View file

@ -11,12 +11,10 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {goToScreen} from 'app/actions/navigation';
export default class AtMention extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
isSearchResult: PropTypes.bool,
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
@ -50,7 +48,6 @@ export default class AtMention extends React.PureComponent {
}
goToUserProfile = () => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
@ -58,7 +55,7 @@ export default class AtMention extends React.PureComponent {
userId: this.state.user.id,
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
};
getUserDetailsFromMentionName(props) {

View file

@ -1,15 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import AtMention from './at_mention';
function mapStateToProps(state) {
@ -20,12 +17,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AtMention);
export default connect(mapStateToProps)(AtMention);

View file

@ -1,538 +0,0 @@
// 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 {intlShape} from 'react-intl';
import {
Alert,
NativeModules,
Platform,
StyleSheet,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import DeviceInfo from 'react-native-device-info';
import AndroidOpenSettings from 'react-native-android-open-settings';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
const ShareExtension = NativeModules.MattermostShare;
export default class AttachmentButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
canTakePhoto: PropTypes.bool,
canTakeVideo: PropTypes.bool,
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
extraOptions: null,
};
static contextTypes = {
intl: intlShape.isRequired,
};
getPermissionDeniedMessage = (source, mediaType = '') => {
const {formatMessage} = this.context.intl;
const applicationName = DeviceInfo.getApplicationName();
switch (source) {
case 'camera': {
if (mediaType === 'video') {
return {
title: formatMessage({
id: 'mobile.camera_video_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_video_permission_denied_description',
defaultMessage: 'Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.camera_photo_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_photo_permission_denied_description',
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
case 'storage':
return {
title: formatMessage({
id: 'mobile.storage_permission_denied_title',
defaultMessage: '{applicationName} would like to access your files',
}, {applicationName}),
text: formatMessage({
id: 'mobile.storage_permission_denied_description',
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
}),
};
case 'video':
return {
title: formatMessage({
id: 'mobile.android.videos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your videos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.videos_permission_denied_description',
defaultMessage: 'Upload videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your video library.',
}),
};
case 'photo':
default: {
if (Platform.OS === 'android') {
return {
title: formatMessage({
id: 'mobile.android.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.photos_permission_denied_description',
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.ios.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.ios.photos_permission_denied_description',
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
}),
};
}
}
}
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('camera', 'photo');
};
attachFileFromCamera = async (source, mediaType) => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('camera', mediaType);
const options = {
quality: 0.8,
videoQuality: 'high',
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
},
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
const hasCameraPermission = await this.hasPhotoPermission(source, mediaType);
if (hasCameraPermission) {
ImagePicker.launchCamera(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachFileFromLibrary = async () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('photo');
const options = {
quality: 0.8,
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
if (Platform.OS === 'ios') {
options.mediaType = 'mixed';
}
const hasPhotoPermission = await this.hasPhotoPermission('photo');
if (hasPhotoPermission) {
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('camera', 'video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('video');
const options = {
videoQuality: 'high',
mediaType: 'video',
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
};
attachFileFromFiles = async () => {
const {browseFileTypes} = this.props;
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
}, async (error, res) => {
if (error) {
return;
}
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
if (newUri.filePath) {
res.uri = newUri.filePath;
} else {
return;
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
}
};
hasPhotoPermission = async (source, mediaType = '') => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const targetSource = source || 'photo';
const hasPermissionToStorage = await Permissions.check(targetSource);
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request(targetSource);
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const canOpenSettings = await Permissions.canOpenSettings();
let grantOption = null;
if (canOpenSettings) {
grantOption = {
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => Permissions.openSettings(),
};
}
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
Alert.alert(
title,
text,
[
grantOption,
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
]
);
return false;
}
}
}
return true;
};
hasStoragePermission = async () => {
if (Platform.OS === 'android') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('storage');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request('storage');
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const {title, text} = this.getPermissionDeniedMessage('storage');
Alert.alert(
title,
text,
[
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
{
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => AndroidOpenSettings.appDetailsSettings(),
},
]
);
return false;
}
}
}
return true;
};
uploadFiles = async (files) => {
const file = files[0];
if (!file.fileSize | !file.fileName) {
const path = (file.path || file.uri).replace('file://', '');
const fileInfo = await RNFetchBlob.fs.stat(path);
file.fileSize = fileInfo.size;
file.fileName = fileInfo.filename;
}
if (!file.type) {
file.type = lookupMimeType(file.fileName);
}
const {validMimeTypes} = this.props;
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
this.props.onShowUnsupportedMimeTypeWarning();
} else if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
canBrowsePhotoLibrary,
canBrowseVideoLibrary,
canTakePhoto,
canTakeVideo,
fileCount,
maxFileCount,
onShowFileMaxWarning,
extraOptions,
actions,
} = this.props;
if (fileCount === maxFileCount) {
onShowFileMaxWarning();
return;
}
this.props.blurTextBox();
const items = [];
if (canTakePhoto) {
items.push({
action: this.attachPhotoFromCamera,
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
},
icon: 'camera',
});
}
if (canTakeVideo) {
items.push({
action: this.attachVideoFromCamera,
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
});
}
if (canBrowsePhotoLibrary) {
items.push({
action: this.attachFileFromLibrary,
text: {
id: t('mobile.file_upload.library'),
defaultMessage: 'Photo Library',
},
icon: 'photo',
});
}
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
action: this.attachVideoFromLibraryAndroid,
text: {
id: t('mobile.file_upload.video'),
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
});
}
if (canBrowseFiles) {
items.push({
action: this.attachFileFromFiles,
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
},
icon: 'file',
});
}
if (extraOptions) {
extraOptions.forEach((option) => {
if (option !== null) {
items.push(option);
}
});
}
actions.showModalOverCurrentContext('OptionsModal', {items});
};
render() {
const {theme, wrapper, children} = this.props;
if (wrapper) {
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
type={'opacity'}
>
{children}
</TouchableWithFeedback>
);
}
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
style={style.buttonContainer}
type={'opacity'}
>
<Icon
size={30}
style={style.attachIcon}
color={changeOpacity(theme.centerChannelColor, 0.9)}
name='md-add'
/>
</TouchableWithFeedback>
);
}
}
const style = StyleSheet.create({
attachIcon: {
marginTop: Platform.select({
ios: 2,
android: 0,
}),
},
buttonContainer: {
height: Platform.select({
ios: 34,
android: 36,
}),
width: 45,
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -1,19 +1,535 @@
// 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 {intlShape} from 'react-intl';
import {
Alert,
NativeModules,
Platform,
StyleSheet,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import DeviceInfo from 'react-native-device-info';
import AndroidOpenSettings from 'react-native-android-open-settings';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import AttachmentButton from './attachment_button';
const ShareExtension = NativeModules.MattermostShare;
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
export default class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
canTakePhoto: PropTypes.bool,
canTakeVideo: PropTypes.bool,
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
extraOptions: null,
};
static contextTypes = {
intl: intlShape.isRequired,
};
getPermissionDeniedMessage = (source, mediaType = '') => {
const {formatMessage} = this.context.intl;
const applicationName = DeviceInfo.getApplicationName();
switch (source) {
case 'camera': {
if (mediaType === 'video') {
return {
title: formatMessage({
id: 'mobile.camera_video_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_video_permission_denied_description',
defaultMessage: 'Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.camera_photo_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_photo_permission_denied_description',
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
case 'storage':
return {
title: formatMessage({
id: 'mobile.storage_permission_denied_title',
defaultMessage: '{applicationName} would like to access your files',
}, {applicationName}),
text: formatMessage({
id: 'mobile.storage_permission_denied_description',
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
}),
};
case 'video':
return {
title: formatMessage({
id: 'mobile.android.videos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your videos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.videos_permission_denied_description',
defaultMessage: 'Upload videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your video library.',
}),
};
case 'photo':
default: {
if (Platform.OS === 'android') {
return {
title: formatMessage({
id: 'mobile.android.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.photos_permission_denied_description',
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.ios.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.ios.photos_permission_denied_description',
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
}),
};
}
}
}
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('camera', 'photo');
};
attachFileFromCamera = async (source, mediaType) => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('camera', mediaType);
const options = {
quality: 0.8,
videoQuality: 'high',
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
},
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
const hasCameraPermission = await this.hasPhotoPermission(source, mediaType);
if (hasCameraPermission) {
ImagePicker.launchCamera(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachFileFromLibrary = async () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('photo');
const options = {
quality: 0.8,
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
if (Platform.OS === 'ios') {
options.mediaType = 'mixed';
}
const hasPhotoPermission = await this.hasPhotoPermission('photo');
if (hasPhotoPermission) {
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('camera', 'video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('video');
const options = {
videoQuality: 'high',
mediaType: 'video',
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
};
attachFileFromFiles = async () => {
const {browseFileTypes} = this.props;
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
}, async (error, res) => {
if (error) {
return;
}
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
if (newUri.filePath) {
res.uri = newUri.filePath;
} else {
return;
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
}
};
hasPhotoPermission = async (source, mediaType = '') => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const targetSource = source || 'photo';
const hasPermissionToStorage = await Permissions.check(targetSource);
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request(targetSource);
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const canOpenSettings = await Permissions.canOpenSettings();
let grantOption = null;
if (canOpenSettings) {
grantOption = {
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => Permissions.openSettings(),
};
}
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
Alert.alert(
title,
text,
[
grantOption,
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
]
);
return false;
}
}
}
return true;
};
hasStoragePermission = async () => {
if (Platform.OS === 'android') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('storage');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request('storage');
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const {title, text} = this.getPermissionDeniedMessage('storage');
Alert.alert(
title,
text,
[
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
{
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => AndroidOpenSettings.appDetailsSettings(),
},
]
);
return false;
}
}
}
return true;
};
uploadFiles = async (files) => {
const file = files[0];
if (!file.fileSize | !file.fileName) {
const path = (file.path || file.uri).replace('file://', '');
const fileInfo = await RNFetchBlob.fs.stat(path);
file.fileSize = fileInfo.size;
file.fileName = fileInfo.filename;
}
if (!file.type) {
file.type = lookupMimeType(file.fileName);
}
const {validMimeTypes} = this.props;
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
this.props.onShowUnsupportedMimeTypeWarning();
} else if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
canBrowsePhotoLibrary,
canBrowseVideoLibrary,
canTakePhoto,
canTakeVideo,
fileCount,
maxFileCount,
onShowFileMaxWarning,
extraOptions,
} = this.props;
if (fileCount === maxFileCount) {
onShowFileMaxWarning();
return;
}
this.props.blurTextBox();
const items = [];
if (canTakePhoto) {
items.push({
action: this.attachPhotoFromCamera,
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
},
icon: 'camera',
});
}
if (canTakeVideo) {
items.push({
action: this.attachVideoFromCamera,
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
});
}
if (canBrowsePhotoLibrary) {
items.push({
action: this.attachFileFromLibrary,
text: {
id: t('mobile.file_upload.library'),
defaultMessage: 'Photo Library',
},
icon: 'photo',
});
}
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
action: this.attachVideoFromLibraryAndroid,
text: {
id: t('mobile.file_upload.video'),
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
});
}
if (canBrowseFiles) {
items.push({
action: this.attachFileFromFiles,
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
},
icon: 'file',
});
}
if (extraOptions) {
extraOptions.forEach((option) => {
if (option !== null) {
items.push(option);
}
});
}
showModalOverCurrentContext('OptionsModal', {items});
};
render() {
const {theme, wrapper, children} = this.props;
if (wrapper) {
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
type={'opacity'}
>
{children}
</TouchableWithFeedback>
);
}
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
style={style.buttonContainer}
type={'opacity'}
>
<Icon
size={30}
style={style.attachIcon}
color={changeOpacity(theme.centerChannelColor, 0.9)}
name='md-add'
/>
</TouchableWithFeedback>
);
}
}
export default connect(null, mapDispatchToProps)(AttachmentButton);
const style = StyleSheet.create({
attachIcon: {
marginTop: Platform.select({
ios: 2,
android: 0,
}),
},
buttonContainer: {
height: Platform.select({
ios: 34,
android: 36,
}),
width: 45,
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -9,9 +9,10 @@ import {Alert} from 'react-native';
import Preferences from 'mattermost-redux/constants/preferences';
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
import AttachmentButton from './attachment_button';
import {PermissionTypes} from 'app/constants';
import AttachmentButton from './index';
jest.mock('react-intl');
jest.mock('Platform', () => {
@ -23,9 +24,6 @@ jest.mock('Platform', () => {
describe('AttachmentButton', () => {
const formatMessage = jest.fn();
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
theme: Preferences.THEMES.default,
blurTextBox: jest.fn(),
maxFileSize: 10,
@ -99,4 +97,4 @@ describe('AttachmentButton', () => {
await wrapper.instance().hasPhotoPermission('camera');
expect(Alert.alert).toBeCalled();
});
});
});

View file

@ -14,12 +14,12 @@ import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {ViewTypes} from 'app/constants';
import {goToScreen} from 'app/actions/navigation';
export default class AutocompleteSelector extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
setAutocompleteSelector: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
}).isRequired,
label: PropTypes.string,
placeholder: PropTypes.string.isRequired,
@ -102,7 +102,7 @@ export default class AutocompleteSelector extends PureComponent {
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
actions.setAutocompleteSelector(dataSource, this.handleSelect, options);
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
render() {

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import {setAutocompleteSelector} from 'app/actions/views/post';
import AutocompleteSelector from './autocomplete_selector';
@ -22,7 +21,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setAutocompleteSelector,
goToScreen,
}, dispatch),
};
}

View file

@ -7,9 +7,10 @@ import {
Text,
View,
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {getFullName} from 'mattermost-redux/utils/user_utils';
import {General} from 'mattermost-redux/constants';
import {injectIntl, intlShape} from 'react-intl';
import BotTag from 'app/components/bot_tag';
import GuestTag from 'app/components/guest_tag';
@ -20,12 +21,10 @@ import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {isGuest} from 'app/utils/users';
import {goToScreen} from 'app/actions/navigation';
class ChannelIntro extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
creator: PropTypes.object,
currentChannel: PropTypes.object.isRequired,
currentChannelMembers: PropTypes.array.isRequired,
@ -39,14 +38,14 @@ class ChannelIntro extends PureComponent {
};
goToUserProfile = (userId) => {
const {actions, intl} = this.props;
const {intl} = this.props;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
userId,
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
};
getDisplayName = (member) => {

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
@ -11,7 +10,6 @@ import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-re
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import {goToScreen} from 'app/actions/navigation';
import {getChannelMembersForDm} from 'app/selectors/channel';
import ChannelIntro from './channel_intro';
@ -55,12 +53,4 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelIntro);
export default connect(makeMapStateToProps)(ChannelIntro);

View file

@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {t} from 'app/utils/i18n';
import {alertErrorWithFallback} from 'app/utils/general';
import {popToRoot, dismissAllModals} from 'app/actions/navigation';
import {getChannelFromChannelName} from './channel_link_utils';
@ -23,10 +24,8 @@ export default class ChannelLink extends React.PureComponent {
textStyle: CustomPropTypes.Style,
channelsByName: PropTypes.object.isRequired,
actions: PropTypes.shape({
dismissAllModals: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
popToRoot: PropTypes.func.isRequired,
}).isRequired,
};
@ -75,10 +74,11 @@ export default class ChannelLink extends React.PureComponent {
}
if (channel.id) {
const {dismissAllModals, handleSelectChannel, popToRoot} = this.props.actions;
const {handleSelectChannel} = this.props.actions;
handleSelectChannel(channel.id);
dismissAllModals();
popToRoot();
await dismissAllModals();
await popToRoot();
if (this.props.onChannelLinkPress) {
this.props.onChannelLinkPress(channel);

View file

@ -11,9 +11,13 @@ import ChannelLink from './channel_link';
jest.mock('react-intl');
jest.mock('app/utils/general', () => ({
alertErrorWithFallback: jest.fn(),
}));
jest.mock('app/utils/general', () => {
const general = require.requireActual('app/utils/general');
return {
...general,
alertErrorWithFallback: jest.fn(),
};
});
describe('ChannelLink', () => {
const formatMessage = jest.fn();
@ -30,10 +34,8 @@ describe('ChannelLink', () => {
textStyle: {color: '#3d3c40', fontSize: 15, lineHeight: 20},
channelsByName,
actions: {
dismissAllModals: jest.fn(),
handleSelectChannel: jest.fn(),
joinChannel: jest.fn(),
popToRoot: jest.fn(),
},
};
@ -71,14 +73,14 @@ describe('ChannelLink', () => {
expect(innerText.props().onPress).not.toBeDefined();
});
test('should call props.actions and onChannelLinkPress on handlePress', () => {
test('should call props.actions and onChannelLinkPress on handlePress', async () => {
const wrapper = shallow(
<ChannelLink {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
const channel = channelsByName.firstChannel;
wrapper.instance().handlePress();
await wrapper.instance().handlePress();
expect(baseProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(baseProps.actions.handleSelectChannel).toBeCalledWith(channel.id);
expect(baseProps.onChannelLinkPress).toHaveBeenCalledTimes(1);

View file

@ -10,7 +10,6 @@ import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entiti
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {dismissAllModals, popToRoot} from 'app/actions/navigation';
import {handleSelectChannel} from 'app/actions/views/channel';
import ChannelLink from './channel_link';
@ -44,10 +43,8 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissAllModals,
handleSelectChannel,
joinChannel,
popToRoot,
}, dispatch),
};
}

View file

@ -17,6 +17,7 @@ import FormattedText from 'app/components/formatted_text';
import {DeviceTypes} from 'app/constants';
import {checkUpgradeType, isUpgradeAvailable} from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {showModal, dismissModal} from 'app/actions/navigation';
const {View: AnimatedView} = Animated;
@ -27,8 +28,6 @@ export default class ClientUpgradeListener extends PureComponent {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
setLastUpgradeCheck: PropTypes.func.isRequired,
showModal: PropTypes.func.isRequired,
dismissModal: PropTypes.func.isRequired,
}).isRequired,
currentVersion: PropTypes.string,
downloadLink: PropTypes.string,
@ -141,10 +140,9 @@ export default class ClientUpgradeListener extends PureComponent {
};
handleLearnMore = () => {
const {actions} = this.props;
const {intl} = this.context;
actions.dismissModal();
dismissModal();
const screen = 'ClientUpgrade';
const title = intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'});
@ -160,7 +158,7 @@ export default class ClientUpgradeListener extends PureComponent {
},
};
actions.showModal(screen, title, passProps, options);
showModal(screen, title, passProps, options);
this.toggleUpgradeMessage(false);
};

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {logError} from 'mattermost-redux/actions/errors';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModal, dismissModal} from 'app/actions/navigation';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {isLandscape} from 'app/selectors/device';
@ -33,8 +32,6 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
logError,
setLastUpgradeCheck,
showModal,
dismissModal,
}, dispatch),
};
}

View file

@ -19,6 +19,7 @@ import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {
changeOpacity,
@ -27,14 +28,10 @@ import {
} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {popTopScreen, dismissModal} from 'app/actions/navigation';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissModal: PropTypes.func.isRequired,
popTopScreen: PropTypes.func.isRequired,
}),
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
@ -97,11 +94,10 @@ export default class EditChannelInfo extends PureComponent {
};
close = (goBack = false) => {
const {actions} = this.props;
if (goBack) {
actions.popTopScreen();
popTopScreen();
} else {
actions.dismissModal();
dismissModal();
}
};

View file

@ -11,10 +11,6 @@ import EditChannelInfo from './edit_channel_info';
describe('EditChannelInfo', () => {
const baseProps = {
actions: {
dismissModal: jest.fn(),
popTopScreen: jest.fn(),
},
theme: Preferences.THEMES.default,
deviceWidth: 400,
deviceHeight: 600,

View file

@ -1,13 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
dismissModal,
popTopScreen,
} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import EditChannelInfo from './edit_channel_info';
@ -17,13 +12,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissModal,
popTopScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EditChannelInfo);
export default connect(mapStateToProps)(EditChannelInfo);

View file

@ -25,6 +25,7 @@ import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {DeviceTypes} from 'app/constants/';
import mattermostBucket from 'app/mattermost_bucket';
import {changeOpacity} from 'app/utils/theme';
import {goToScreen} from 'app/actions/navigation';
const {DOCUMENTS_PATH} = DeviceTypes;
const DOWNLOADING_OFFSET = 28;
@ -37,9 +38,6 @@ const circularProgressWidth = 4;
export default class FileAttachmentDocument extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
backgroundColor: PropTypes.string,
canDownloadFiles: PropTypes.bool.isRequired,
iconHeight: PropTypes.number,
@ -193,13 +191,14 @@ export default class FileAttachmentDocument extends PureComponent {
};
previewTextFile = (file, delay = 2000) => {
const {actions} = this.props;
const {data} = file;
const prefix = Platform.OS === 'android' ? 'file:/' : '';
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
const readFile = RNFetchBlob.fs.readFile(`${prefix}${path}`, 'utf8');
setTimeout(async () => {
try {
this.setState({downloading: false, progress: 0});
const content = await readFile;
const screen = 'TextPreview';
const title = file.caption;
@ -207,8 +206,7 @@ export default class FileAttachmentDocument extends PureComponent {
content,
};
actions.goToScreen(screen, title, passProps);
this.setState({downloading: false, progress: 0});
goToScreen(screen, title, passProps);
} catch (error) {
RNFetchBlob.fs.unlink(path);
}

View file

@ -1,19 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToScreen} from 'app/actions/navigation';
import FileAttachmentDocument from './file_attachment_document';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps, null, {forwardRef: true})(FileAttachmentDocument);
export default connect(null, null, null, {forwardRef: true})(FileAttachmentDocument);

View file

@ -22,7 +22,6 @@ export default class FileAttachmentList extends Component {
static propTypes = {
actions: PropTypes.shape({
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
canDownloadFiles: PropTypes.bool.isRequired,
fileIds: PropTypes.array.isRequired,
@ -122,8 +121,7 @@ export default class FileAttachmentList extends Component {
};
handlePreviewPress = preventDoubleTap((idx) => {
const {actions} = this.props;
previewImageAtIndex(this.items, idx, this.galleryFiles, actions.showModalOverCurrentContext);
previewImageAtIndex(this.items, idx, this.galleryFiles);
});
renderItems = () => {

View file

@ -16,7 +16,6 @@ describe('PostAttachmentOpenGraph', () => {
const baseProps = {
actions: {
loadFilesForPostIfNecessary,
showModalOverCurrentContext: jest.fn(),
},
canDownloadFiles: true,
deviceHeight: 680,
@ -72,7 +71,6 @@ describe('PostAttachmentOpenGraph', () => {
files: [],
actions: {
loadFilesForPostIfNecessary: loadFilesForPostIfNecessaryMock,
showModalOverCurrentContext: jest.fn(),
},
};

View file

@ -8,7 +8,6 @@ import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/gene
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
import FileAttachmentList from './file_attachment_list';
@ -28,7 +27,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadFilesForPostIfNecessary,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -6,8 +6,6 @@ import {connect} from 'react-redux';
import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations';
import {showModal} from 'app/actions/navigation';
import InteractiveDialogController from './interactive_dialog_controller';
function mapStateToProps(state) {
@ -20,7 +18,6 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModal,
submitInteractiveDialog,
}, dispatch),
};

View file

@ -7,10 +7,11 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {showModal} from 'app/actions/navigation';
export default class InteractiveDialogController extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModal: PropTypes.func.isRequired,
submitInteractiveDialog: PropTypes.func.isRequired,
}).isRequired,
triggerId: PropTypes.string,
@ -88,7 +89,7 @@ export default class InteractiveDialogController extends PureComponent {
},
};
this.props.actions.showModal('InteractiveDialog', dialog.title, null, options);
showModal('InteractiveDialog', dialog.title, null, options);
}
handleCancel = (dialog, url) => {

View file

@ -73,7 +73,6 @@ function getBaseProps(triggerId, elements, introductionText) {
return {
actions: {
showModal: jest.fn(),
submitInteractiveDialog: jest.fn(),
},
triggerId,

View file

@ -1,52 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class Hashtag extends React.PureComponent {
static propTypes = {
hashtag: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style.isRequired,
onHashtagPress: PropTypes.func,
actions: PropTypes.shape({
popToRoot: PropTypes.func.isRequired,
showSearchModal: PropTypes.func.isRequired,
dismissAllModals: PropTypes.func.isRequired,
}).isRequired,
};
handlePress = () => {
const {
onHashtagPress,
hashtag,
actions,
} = this.props;
if (onHashtagPress) {
onHashtagPress(hashtag);
return;
}
// Close thread view, permalink view, etc
actions.dismissAllModals();
actions.popToRoot();
actions.showSearchModal('#' + this.props.hashtag);
};
render() {
return (
<Text
style={this.props.linkStyle}
onPress={this.handlePress}
>
{`#${this.props.hashtag}`}
</Text>
);
}
}

View file

@ -1,57 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Text} from 'react-native';
import Hashtag from './hashtag';
describe('Hashtag', () => {
const baseProps = {
hashtag: 'test',
linkStyle: {color: 'red'},
actions: {
showSearchModal: jest.fn(),
dismissAllModals: jest.fn(),
popToRoot: jest.fn(),
},
};
test('should match snapshot', () => {
const wrapper = shallow(<Hashtag {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should open hashtag search on click', () => {
const props = {
...baseProps,
};
const wrapper = shallow(<Hashtag {...props}/>);
wrapper.find(Text).simulate('press');
expect(props.actions.dismissAllModals).toHaveBeenCalled();
expect(props.actions.popToRoot).toHaveBeenCalled();
expect(props.actions.showSearchModal).toHaveBeenCalledWith('#test');
});
test('should call onHashtagPress if provided', () => {
const props = {
...baseProps,
onHashtagPress: jest.fn(),
};
const wrapper = shallow(<Hashtag {...props}/>);
wrapper.find(Text).simulate('press');
expect(props.actions.dismissAllModals).not.toBeCalled();
expect(props.actions.popToRoot).not.toBeCalled();
expect(props.actions.showSearchModal).not.toBeCalled();
expect(props.onHashtagPress).toBeCalled();
});
});

View file

@ -1,25 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import React from 'react';
import {Text} from 'react-native';
import {
popToRoot,
showSearchModal,
dismissAllModals,
} from 'app/actions/navigation';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {popToRoot, showSearchModal, dismissAllModals} from 'app/actions/navigation';
import Hashtag from './hashtag';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
popToRoot,
showSearchModal,
dismissAllModals,
}, dispatch),
export default class Hashtag extends React.PureComponent {
static propTypes = {
hashtag: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style.isRequired,
onHashtagPress: PropTypes.func,
};
}
export default connect(null, mapDispatchToProps)(Hashtag);
handlePress = async () => {
const {
onHashtagPress,
hashtag,
} = this.props;
if (onHashtagPress) {
onHashtagPress(hashtag);
return;
}
// Close thread view, permalink view, etc
await dismissAllModals();
await popToRoot();
showSearchModal('#' + this.props.hashtag);
};
render() {
return (
<Text
style={this.props.linkStyle}
onPress={this.handlePress}
>
{`#${this.props.hashtag}`}
</Text>
);
}
}

View file

@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import * as NavigationActions from 'app/actions/navigation';
import Hashtag from './index';
describe('Hashtag', () => {
const baseProps = {
hashtag: 'test',
linkStyle: {color: 'red'},
};
test('should match snapshot', () => {
const wrapper = shallow(<Hashtag {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('handlePress should open hashtag search', async () => {
const dismissAllModals = jest.spyOn(NavigationActions, 'dismissAllModals');
const popToRoot = jest.spyOn(NavigationActions, 'popToRoot');
const showSearchModal = jest.spyOn(NavigationActions, 'showSearchModal');
const props = {
...baseProps,
};
const wrapper = shallow(<Hashtag {...props}/>);
await wrapper.instance().handlePress();
expect(dismissAllModals).toHaveBeenCalled();
expect(popToRoot).toHaveBeenCalled();
expect(showSearchModal).toHaveBeenCalledWith('#test');
});
test('handlePress should call onHashtagPress if provided', async () => {
const dismissAllModals = jest.spyOn(NavigationActions, 'dismissAllModals');
const popToRoot = jest.spyOn(NavigationActions, 'popToRoot');
const showSearchModal = jest.spyOn(NavigationActions, 'showSearchModal');
const props = {
...baseProps,
onHashtagPress: jest.fn(),
};
const wrapper = shallow(<Hashtag {...props}/>);
await wrapper.instance().handlePress();
expect(dismissAllModals).not.toBeCalled();
expect(popToRoot).not.toBeCalled();
expect(showSearchModal).not.toBeCalled();
expect(props.onHashtagPress).toBeCalled();
});
});

View file

@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownCodeBlock from './markdown_code_block';
function mapStateToProps(state) {
@ -16,12 +13,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownCodeBlock);
export default connect(mapStateToProps)(MarkdownCodeBlock);

View file

@ -19,14 +19,12 @@ import {getDisplayNameForLanguage} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import mattermostManaged from 'app/mattermost_managed';
import {goToScreen} from 'app/actions/navigation';
const MAX_LINES = 4;
export default class MarkdownCodeBlock extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
theme: PropTypes.object.isRequired,
language: PropTypes.string,
content: PropTypes.string.isRequired,
@ -42,7 +40,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions, language, content} = this.props;
const {language, content} = this.props;
const {intl} = this.context;
const screen = 'Code';
const passProps = {
@ -68,7 +66,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
});
}
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
handleLongPress = async () => {

View file

@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import MarkdownImage from './markdown_image';
@ -19,12 +16,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownImage);
export default connect(mapStateToProps)(MarkdownImage);

View file

@ -34,9 +34,6 @@ const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
export default class MarkdownImage extends React.Component {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
@ -177,7 +174,6 @@ export default class MarkdownImage extends React.Component {
originalWidth,
uri,
} = this.state;
const {actions} = this.props;
const link = this.getSource();
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
const extension = filename.split('.').pop();
@ -199,7 +195,7 @@ export default class MarkdownImage extends React.Component {
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([this.refs.item], 0, files);
};
loadImageSize = (source) => {

View file

@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownTable from './markdown_table';
function mapStateToProps(state) {
@ -16,12 +13,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTable);
export default connect(mapStateToProps)(MarkdownTable);

View file

@ -11,17 +11,16 @@ import {
import LinearGradient from 'react-native-linear-gradient';
import {CELL_WIDTH} from 'app/components/markdown/markdown_table_cell/markdown_table_cell';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {goToScreen} from 'app/actions/navigation';
const MAX_HEIGHT = 300;
export default class MarkdownTable extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node.isRequired,
numColumns: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
@ -46,7 +45,6 @@ export default class MarkdownTable extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'Table';
const title = intl.formatMessage({
@ -58,7 +56,7 @@ export default class MarkdownTable extends React.PureComponent {
tableWidth: this.getTableWidth(),
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
handleContainerLayout = (e) => {

View file

@ -1,14 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownTableImage from './markdown_table_image';
function mapStateToProps(state) {
@ -18,12 +15,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTableImage);
export default connect(mapStateToProps)(MarkdownTableImage);

View file

@ -9,12 +9,10 @@ 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';
import {goToScreen} from 'app/actions/navigation';
export default class MarkdownTableImage extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node.isRequired,
source: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style.isRequired,
@ -27,7 +25,6 @@ export default class MarkdownTableImage extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'TableImage';
const title = intl.formatMessage({
@ -38,7 +35,7 @@ export default class MarkdownTableImage extends React.PureComponent {
imageSource: this.getImageSource(),
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
getImageSource = async () => {

View file

@ -258,8 +258,6 @@ function getLastSibling(node) {
export function highlightMentions(ast, mentionKeys) {
const walker = ast.walker();
// console.warn(mentionKeys);
let e;
while ((e = walker.next())) {
if (!e.entering) {

View file

@ -2966,28 +2966,28 @@ describe('Components.Markdown.transform', () => {
// Confirms that all parent, child, and sibling linkages are correct and go both ways.
function verifyAst(node) {
if (node.prev && node.prev.next !== node) {
console.error('node is not linked properly to prev');
console.error('node is not linked properly to prev'); //eslint-disable-line no-console
return false;
}
if (node.next && node.next.prev !== node) {
console.error('node is not linked properly to next');
console.error('node is not linked properly to next'); //eslint-disable-line no-console
return false;
}
if (!node.firstChild && node.lastChild) {
console.error('node has children, but is not linked to first child');
console.error('node has children, but is not linked to first child'); //eslint-disable-line no-console
return false;
}
if (node.firstChild && !node.lastChild) {
console.error('node has children, but is not linked to last child');
console.error('node has children, but is not linked to last child'); //eslint-disable-line no-console
return false;
}
for (let child = node.firstChild; child; child = child.next) {
if (child.parent !== node) {
console.error('node is not linked properly to child');
console.error('node is not linked properly to child'); //eslint-disable-line no-console
return false;
}
@ -2996,18 +2996,18 @@ function verifyAst(node) {
}
if (!child.next && child !== node.lastChild) {
console.error('node children are not linked correctly');
console.error('node children are not linked correctly'); //eslint-disable-line no-console
return false;
}
}
if (node.firstChild && node.firstChild.prev) {
console.error('node\'s first child has previous sibling');
console.error('node\'s first child has previous sibling'); //eslint-disable-line no-console
return false;
}
if (node.lastChild && node.lastChild.next) {
console.error('node\'s last child has next sibling');
console.error('node\'s last child has next sibling'); //eslint-disable-line no-console
return false;
}

View file

@ -1,178 +0,0 @@
// 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 {Image, View} from 'react-native';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const VIEWPORT_IMAGE_OFFSET = 100;
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
export default class AttachmentImage extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
imageUrl: PropTypes.string,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
hasImage: Boolean(props.imageUrl),
imageUri: null,
};
}
componentDidMount() {
this.mounted = true;
const {imageUrl, imageMetadata} = this.props;
this.setViewPortMaxWidth();
if (imageMetadata) {
this.setImageDimensionsFromMeta(null, imageMetadata);
}
if (imageUrl) {
ImageCacheManager.cache(null, imageUrl, this.setImageUrl);
}
}
componentDidUpdate(prevProps) {
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl);
}
}
handlePreviewImage = () => {
const {actions, imageUrl} = this.props;
const {
imageUri: uri,
originalHeight,
originalWidth,
} = this.state;
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const files = [{
caption: filename,
dimensions: {
height: originalHeight,
width: originalWidth,
},
source: {uri},
data: {
localPath: uri,
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
...dimensions,
originalWidth,
originalHeight,
imageUri,
});
}
};
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
};
setImageUrl = (imageURL) => {
const {imageMetadata} = this.props;
if (imageMetadata) {
this.setImageDimensionsFromMeta(imageURL, imageMetadata);
return;
}
Image.getSize(imageURL, (width, height) => {
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
this.setImageDimensions(imageURL, dimensions, width, height);
}, () => null);
};
setViewPortMaxWidth = () => {
const {deviceWidth, deviceHeight} = this.props;
const viewPortWidth = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET;
};
render() {
const {imageMetadata, theme} = this.props;
const {hasImage, height, imageUri, width} = this.state;
if (!hasImage || isGifTooLarge(imageMetadata)) {
return null;
}
const style = getStyleSheet(theme);
let progressiveImage;
if (imageUri) {
progressiveImage = (
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
);
} else {
progressiveImage = (<View style={{width, height}}/>);
}
return (
<TouchableWithFeedback
onPress={this.handlePreviewImage}
style={[style.container, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
type={'none'}
>
<View
ref='item'
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}
</View>
</TouchableWithFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginTop: 5,
},
imageContainer: {
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
borderRadius: 2,
flex: 1,
padding: 5,
},
};
});

View file

@ -1,19 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, View} from 'react-native';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import AttachmentImage from './attachment_image';
const VIEWPORT_IMAGE_OFFSET = 100;
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
export default class AttachmentImage extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
imageUrl: PropTypes.string,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
hasImage: Boolean(props.imageUrl),
imageUri: null,
};
}
componentDidMount() {
this.mounted = true;
const {imageUrl, imageMetadata} = this.props;
this.setViewPortMaxWidth();
if (imageMetadata) {
this.setImageDimensionsFromMeta(null, imageMetadata);
}
if (imageUrl) {
ImageCacheManager.cache(null, imageUrl, this.setImageUrl);
}
}
componentDidUpdate(prevProps) {
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl);
}
}
handlePreviewImage = () => {
const {imageUrl} = this.props;
const {
imageUri: uri,
originalHeight,
originalWidth,
} = this.state;
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const files = [{
caption: filename,
dimensions: {
height: originalHeight,
width: originalWidth,
},
source: {uri},
data: {
localPath: uri,
},
}];
previewImageAtIndex([this.refs.item], 0, files);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
...dimensions,
originalWidth,
originalHeight,
imageUri,
});
}
};
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
};
setImageUrl = (imageURL) => {
const {imageMetadata} = this.props;
if (imageMetadata) {
this.setImageDimensionsFromMeta(imageURL, imageMetadata);
return;
}
Image.getSize(imageURL, (width, height) => {
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
this.setImageDimensions(imageURL, dimensions, width, height);
}, () => null);
};
setViewPortMaxWidth = () => {
const {deviceWidth, deviceHeight} = this.props;
const viewPortWidth = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET;
};
render() {
const {imageMetadata, theme} = this.props;
const {hasImage, height, imageUri, width} = this.state;
if (!hasImage || isGifTooLarge(imageMetadata)) {
return null;
}
const style = getStyleSheet(theme);
let progressiveImage;
if (imageUri) {
progressiveImage = (
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
);
} else {
progressiveImage = (<View style={{width, height}}/>);
}
return (
<TouchableWithFeedback
onPress={this.handlePreviewImage}
style={[style.container, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
type={'none'}
>
<View
ref='item'
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}
</View>
</TouchableWithFeedback>
);
}
}
export default connect(null, mapDispatchToProps)(AttachmentImage);
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginTop: 5,
},
imageContainer: {
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
borderRadius: 2,
flex: 1,
padding: 5,
},
};
});

View file

@ -11,13 +11,10 @@ const originalGetSizeFn = Image.getSize;
import Preferences from 'mattermost-redux/constants/preferences';
import AttachmentImage from './attachment_image';
import AttachmentImage from './index';
describe('AttachmentImage', () => {
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
deviceHeight: 256,
deviceWidth: 128,
imageMetadata: {width: 32, height: 32},

View file

@ -12,7 +12,6 @@ import {getUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/use
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
@ -96,8 +95,6 @@ function mapDispatchToProps(dispatch) {
removePost,
setPostTooltipVisible,
insertToDraft,
goToScreen,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -11,6 +11,10 @@ import {
} from 'react-native';
import {intlShape} from 'react-intl';
import {Posts} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import PostBody from 'app/components/post_body';
import PostHeader from 'app/components/post_header';
import PostPreHeader from 'app/components/post_header/post_pre_header';
@ -22,10 +26,7 @@ import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {Posts} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import Config from 'assets/config';
@ -35,8 +36,6 @@ export default class Post extends PureComponent {
createPost: PropTypes.func.isRequired,
insertToDraft: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
channelIsReadOnly: PropTypes.bool,
currentUserId: PropTypes.string.isRequired,
@ -91,7 +90,7 @@ export default class Post extends PureComponent {
goToUserProfile = () => {
const {intl} = this.context;
const {actions, post} = this.props;
const {post} = this.props;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
@ -100,7 +99,7 @@ export default class Post extends PureComponent {
Keyboard.dismiss();
requestAnimationFrame(() => {
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
};
@ -141,7 +140,7 @@ export default class Post extends PureComponent {
}],
};
this.props.actions.showModalOverCurrentContext(screen, passProps);
showModalOverCurrentContext(screen, passProps);
};
handlePress = preventDoubleTap(() => {

View file

@ -6,8 +6,6 @@ import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import PostAttachmentOpenGraph from './post_attachment_opengraph';
@ -22,7 +20,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getOpenGraphMetadata,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -27,7 +27,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getOpenGraphMetadata: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
@ -194,7 +193,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
originalWidth,
originalHeight,
} = this.state;
const {actions} = this.props;
const filename = this.getFilename(link);
const files = [{
@ -209,7 +207,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([this.refs.item], 0, files);
};
renderDescription = () => {

View file

@ -22,7 +22,6 @@ describe('PostAttachmentOpenGraph', () => {
const baseProps = {
actions: {
getOpenGraphMetadata: jest.fn(),
showModalOverCurrentContext: jest.fn(),
},
deviceHeight: 600,
deviceWidth: 400,

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {General, Posts} from 'mattermost-redux/constants';
@ -22,8 +21,6 @@ import {
} from 'mattermost-redux/utils/post_utils';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import {hasEmojisOnly} from 'app/utils/emoji_utils';
@ -106,12 +103,4 @@ export function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostBody);
export default connect(makeMapStateToProps, null, null, {forwardRef: true})(PostBody);

View file

@ -22,6 +22,7 @@ import {emptyFunction} from 'app/utils/general';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import telemetry from 'app/telemetry';
@ -34,9 +35,6 @@ const SHOW_MORE_HEIGHT = 60;
export default class PostBody extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
canDelete: PropTypes.bool,
channelIsReadOnly: PropTypes.bool.isRequired,
deviceHeight: PropTypes.number.isRequired,
@ -133,7 +131,6 @@ export default class PostBody extends PureComponent {
onHashtagPress,
onPermalinkPress,
post,
actions,
} = this.props;
const screen = 'LongPost';
const passProps = {
@ -148,7 +145,7 @@ export default class PostBody extends PureComponent {
},
};
actions.showModalOverCurrentContext(screen, passProps, options);
showModalOverCurrentContext(screen, passProps, options);
});
showPostOptions = () => {
@ -165,7 +162,6 @@ export default class PostBody extends PureComponent {
post,
showAddReaction,
location,
actions,
} = this.props;
if (isSystemMessage && (!canDelete || hasBeenDeleted)) {
@ -191,7 +187,7 @@ export default class PostBody extends PureComponent {
Keyboard.dismiss();
requestAnimationFrame(() => {
actions.showModalOverCurrentContext(screen, passProps);
showModalOverCurrentContext(screen, passProps);
});
};

View file

@ -12,9 +12,6 @@ import PostBody from './post_body.js';
describe('PostBody', () => {
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
canDelete: true,
channelIsReadOnly: false,
deviceHeight: 1920,

View file

@ -10,7 +10,6 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {ViewTypes} from 'app/constants';
import {getDimensions} from 'app/selectors/device';
import {extractFirstLink} from 'app/utils/url';
@ -76,7 +75,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getRedirectLocation,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -38,7 +38,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getRedirectLocation: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
@ -412,7 +411,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
handlePreviewImage = (imageRef) => {
const {shortenedLink} = this.state;
let {link} = this.props;
const {actions} = this.props;
if (shortenedLink) {
link = shortenedLink;
}
@ -434,7 +432,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
},
}];
previewImageAtIndex([imageRef], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([imageRef], 0, files);
};
playYouTubeVideo = () => {

View file

@ -9,7 +9,6 @@ import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/gene
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'mattermost-redux/utils/post_list';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {handleSelectChannelByName, loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
import {setDeepLinkURL} from 'app/actions/views/root';
@ -43,7 +42,6 @@ function mapDispatchToProps(dispatch) {
refreshChannelWithRetry,
selectFocusedPostId,
setDeepLinkURL,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -16,6 +16,7 @@ import {makeExtraData} from 'app/utils/list_view';
import {changeOpacity} from 'app/utils/theme';
import {matchDeepLink} from 'app/utils/url';
import telemetry from 'app/telemetry';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import DateHeader from './date_header';
import NewMessagesDivider from './new_messages_divider';
@ -41,7 +42,6 @@ export default class PostList extends PureComponent {
refreshChannelWithRetry: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
setDeepLinkURL: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string,
deepLinkURL: PropTypes.string,
@ -353,7 +353,7 @@ export default class PostList extends PureComponent {
};
this.showingPermalink = true;
actions.showModalOverCurrentContext(screen, passProps, options);
showModalOverCurrentContext(screen, passProps, options);
}
};

View file

@ -4,9 +4,11 @@
import React from 'react';
import {shallow} from 'enzyme';
import PostList from './post_list';
import Preferences from 'mattermost-redux/constants/preferences';
import * as NavigationActions from 'app/actions/navigation';
import PostList from './post_list';
jest.useFakeTimers();
describe('PostList', () => {
@ -18,7 +20,6 @@ describe('PostList', () => {
refreshChannelWithRetry: jest.fn(),
selectFocusedPostId: jest.fn(),
setDeepLinkURL: jest.fn(),
showModalOverCurrentContext: jest.fn(),
},
deepLinkURL: '',
lastPostIndex: -1,
@ -42,10 +43,12 @@ describe('PostList', () => {
});
test('setting permalink deep link', () => {
const showModalOverCurrentContext = jest.spyOn(NavigationActions, 'showModalOverCurrentContext');
wrapper.setProps({deepLinkURL: deepLinks.permalink});
expect(baseProps.actions.setDeepLinkURL).toHaveBeenCalled();
expect(baseProps.actions.selectFocusedPostId).toHaveBeenCalled();
expect(baseProps.actions.showModalOverCurrentContext).toHaveBeenCalled();
expect(showModalOverCurrentContext).toHaveBeenCalled();
expect(wrapper.getElement()).toMatchSnapshot();
});

View file

@ -19,8 +19,15 @@ exports[`PostTextBox should match, full snapshot 1`] = `
]
}
>
<Connect(AttachmentButton)
<AttachmentButton
blurTextBox={[Function]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={null}
fileCount={0}
maxFileCount={5}
maxFileSize={1024}
@ -55,6 +62,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
}
}
uploadFiles={[Function]}
validMimeTypes={Array []}
/>
<View
style={

View file

@ -13,7 +13,6 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {showModal, showModalOverCurrentContext} from 'app/actions/navigation';
import {addReaction} from 'app/actions/views/emoji';
import Reactions from './reactions';
@ -64,8 +63,6 @@ function mapDispatchToProps(dispatch) {
addReaction,
getReactionsForPost,
removeReaction,
showModal,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -13,6 +13,8 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {showModal, showModalOverCurrentContext} from 'app/actions/navigation';
import addReactionIcon from 'assets/images/icons/reaction.png';
import Reaction from './reaction';
@ -23,8 +25,6 @@ export default class Reactions extends PureComponent {
addReaction: PropTypes.func.isRequired,
getReactionsForPost: PropTypes.func.isRequired,
removeReaction: PropTypes.func.isRequired,
showModal: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
currentUserId: PropTypes.string.isRequired,
position: PropTypes.oneOf(['right', 'left']),
@ -51,7 +51,7 @@ export default class Reactions extends PureComponent {
}
handleAddReaction = preventDoubleTap(() => {
const {actions, theme} = this.props;
const {theme} = this.props;
const {formatMessage} = this.context.intl;
const screen = 'AddReaction';
const title = formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
@ -62,7 +62,7 @@ export default class Reactions extends PureComponent {
onEmojiPress: this.handleAddReactionToPost,
};
actions.showModal(screen, title, passProps);
showModal(screen, title, passProps);
});
});
@ -86,7 +86,7 @@ export default class Reactions extends PureComponent {
};
showReactionList = () => {
const {actions, postId} = this.props;
const {postId} = this.props;
const screen = 'ReactionList';
const passProps = {
@ -94,7 +94,7 @@ export default class Reactions extends PureComponent {
};
if (!this.onPressDetected) {
actions.showModalOverCurrentContext(screen, passProps);
showModalOverCurrentContext(screen, passProps);
}
};

View file

@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {resetToTeams} from 'app/actions/navigation';
import {getCurrentLocale} from 'app/selectors/i18n';
import {removeProtocol} from 'app/utils/url';
@ -23,12 +21,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
resetToTeams,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Root);
export default connect(mapStateToProps)(Root);

View file

@ -9,14 +9,12 @@ import {Platform} from 'react-native';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {resetToTeams} from 'app/actions/navigation';
import {NavigationTypes} from 'app/constants';
import {getTranslations} from 'app/i18n';
export default class Root extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
resetToTeams: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node,
excludeEvents: PropTypes.bool,
currentUrl: PropTypes.string,
@ -63,7 +61,7 @@ export default class Root extends PureComponent {
}
navigateToTeamsPage = (screen) => {
const {currentUrl, theme, actions} = this.props;
const {currentUrl, theme} = this.props;
const {intl} = this.refs.provider.getChildContext();
let passProps = {theme};
@ -93,7 +91,7 @@ export default class Root extends PureComponent {
const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'});
actions.resetToTeams(screen, title, passProps, options);
resetToTeams(screen, title, passProps, options);
}
render() {

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
@ -20,7 +19,6 @@ import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selecto
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
import Permissions from 'mattermost-redux/constants/permissions';
import {showModal} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import {DeviceTypes, ViewTypes} from 'app/constants';
@ -77,14 +75,6 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModal,
}, dispatch),
};
}
function areStatesEqual(next, prev) {
const equalRoles = getCurrentUserRoles(prev) === getCurrentUserRoles(next);
const equalChannels = next.entities.channels === prev.entities.channels;
@ -95,4 +85,4 @@ function areStatesEqual(next, prev) {
return equalChannels && equalConfig && equalRoles && equalUsers && equalFav;
}
export default connect(mapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(List);
export default connect(mapStateToProps, null, null, {pure: true, areStatesEqual})(List);

View file

@ -21,14 +21,14 @@ import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {DeviceTypes, ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import BottomSheet from 'app/utils/bottom_sheet';
import {t} from 'app/utils/i18n';
import {preventDoubleTap} from 'app/utils/tap';
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {showModal} from 'app/actions/navigation';
const VIEWABILITY_CONFIG = {
...ListTypes.VISIBILITY_CONFIG_DEFAULTS,
@ -50,9 +50,6 @@ export default class List extends PureComponent {
orderedChannelIds: PropTypes.array.isRequired,
previewChannel: PropTypes.func,
isLandscape: PropTypes.bool.isRequired,
actions: PropTypes.shape({
showModal: PropTypes.func.isRequired,
}).isRequired,
};
static contextTypes = {
@ -224,7 +221,6 @@ export default class List extends PureComponent {
};
goToCreatePublicChannel = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'CreateChannel';
const title = intl.formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'});
@ -233,11 +229,10 @@ export default class List extends PureComponent {
closeButton: this.closeButton,
};
actions.showModal(screen, title, passProps);
showModal(screen, title, passProps);
});
goToCreatePrivateChannel = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'CreateChannel';
const title = intl.formatMessage({id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'});
@ -246,11 +241,10 @@ export default class List extends PureComponent {
closeButton: this.closeButton,
};
actions.showModal(screen, title, passProps);
showModal(screen, title, passProps);
});
goToDirectMessages = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'MoreDirectMessages';
const title = intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'});
@ -264,11 +258,10 @@ export default class List extends PureComponent {
},
};
actions.showModal(screen, title, passProps, options);
showModal(screen, title, passProps, options);
});
goToMoreChannels = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'MoreChannels';
const title = intl.formatMessage({id: 'more_channels.title', defaultMessage: 'More Channels'});
@ -276,7 +269,7 @@ export default class List extends PureComponent {
closeButton: this.closeButton,
};
actions.showModal(screen, title, passProps);
showModal(screen, title, passProps);
});
keyExtractor = (item) => item.id || item;

View file

@ -7,7 +7,6 @@ import {connect} from 'react-redux';
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';
@ -28,7 +27,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleTeamChange,
showModal,
}, dispatch),
};
}

View file

@ -23,6 +23,7 @@ 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';
import {showModal} from 'app/actions/navigation';
import TeamsListItem from './teams_list_item';
@ -36,7 +37,6 @@ export default class TeamsList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired,
showModal: PropTypes.func.isRequired,
}).isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
@ -86,7 +86,7 @@ export default class TeamsList extends PureComponent {
goToSelectTeam = preventDoubleTap(async () => {
const {intl} = this.context;
const {theme, actions} = this.props;
const {theme} = this.props;
const {serverUrl} = this.state;
const screen = 'SelectTeam';
const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'});
@ -103,7 +103,7 @@ export default class TeamsList extends PureComponent {
},
};
actions.showModal(screen, title, passProps, options);
showModal(screen, title, passProps, options);
});
keyExtractor = (item) => {

View file

@ -8,12 +8,6 @@ import {logout, setStatus} from 'mattermost-redux/actions/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import {
showModal,
showModalOverCurrentContext,
dismissModal,
} from 'app/actions/navigation';
import SettingsSidebar from './settings_sidebar';
function mapStateToProps(state) {
@ -32,9 +26,6 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
logout,
setStatus,
showModal,
showModalOverCurrentContext,
dismissModal,
}, dispatch),
};
}

View file

@ -25,6 +25,7 @@ import {confirmOutOfOfficeDisabled} from 'app/utils/status';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {showModal, showModalOverCurrentContext, dismissModal} from 'app/actions/navigation';
import DrawerItem from './drawer_item';
import UserInfo from './user_info';
@ -35,9 +36,6 @@ export default class SettingsDrawer extends PureComponent {
actions: PropTypes.shape({
logout: PropTypes.func.isRequired,
setStatus: PropTypes.func.isRequired,
showModal: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
dismissModal: PropTypes.func.isRequired,
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
@ -127,7 +125,6 @@ export default class SettingsDrawer extends PureComponent {
};
handleSetStatus = preventDoubleTap(() => {
const {actions} = this.props;
const items = [{
action: () => this.setStatus(General.ONLINE),
text: {
@ -154,7 +151,7 @@ export default class SettingsDrawer extends PureComponent {
},
}];
actions.showModalOverCurrentContext('OptionsModal', {items});
showModalOverCurrentContext('OptionsModal', {items});
});
goToEditProfile = preventDoubleTap(() => {
@ -216,7 +213,6 @@ export default class SettingsDrawer extends PureComponent {
openModal = (screen, title, passProps) => {
this.closeSettingsSidebar();
const {actions} = this.props;
const options = {
topBar: {
leftButtons: [{
@ -227,7 +223,7 @@ export default class SettingsDrawer extends PureComponent {
};
InteractionManager.runAfterInteractions(() => {
actions.showModal(screen, title, passProps, options);
showModal(screen, title, passProps, options);
});
};
@ -354,10 +350,10 @@ export default class SettingsDrawer extends PureComponent {
};
setStatus = (status) => {
const {status: currentUserStatus, actions} = this.props;
const {status: currentUserStatus} = this.props;
if (currentUserStatus === General.OUT_OF_OFFICE) {
actions.dismissModal();
dismissModal();
this.closeSettingsSidebar();
this.confirmReset(status);
return;

View file

@ -247,7 +247,7 @@ class GlobalEventHandler {
handleInAppNotification = (notification) => {
const {data} = notification;
const {dispatch, getState} = this.store;
const {getState} = this.store;
const state = getState();
const currentChannelId = getCurrentChannelId(state);
@ -258,7 +258,7 @@ class GlobalEventHandler {
};
EventEmitter.emit(NavigationTypes.NAVIGATION_SHOW_OVERLAY);
dispatch(showOverlay(screen, passProps));
showOverlay(screen, passProps);
}
};
}

View file

@ -9,21 +9,19 @@ import {loadMe} from 'mattermost-redux/actions/users';
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
import {setDeepLinkURL} from 'app/actions/views/root';
import initialState from 'app/initial_state';
import {getAppCredentials} from 'app/init/credentials';
import emmProvider from 'app/init/emm_provider';
import 'app/init/device';
import 'app/init/fetch';
import globalEventHandler from 'app/init/global_event_handler';
import {registerScreens} from 'app/screens';
import configureStore from 'app/store';
import store from 'app/store';
import EphemeralStore from 'app/store/ephemeral_store';
import telemetry from 'app/telemetry';
import pushNotificationsUtils from 'app/utils/push_notifications';
const {MattermostShare} = NativeModules;
const sharedExtensionStarted = Platform.OS === 'android' && MattermostShare.isOpened;
export const store = configureStore(initialState);
const init = async () => {
const credentials = await getAppCredentials();
@ -57,9 +55,9 @@ const launchApp = async (credentials) => {
if (credentials) {
store.dispatch(loadMe());
store.dispatch(resetToChannel({skipMetrics: true}));
resetToChannel({skipMetrics: true});
} else {
store.dispatch(resetToSelectServer(emmProvider.allowOtherServers));
resetToSelectServer(emmProvider.allowOtherServers);
}
telemetry.startSinceLaunch(['start:splash_screen']);

View file

@ -12,13 +12,10 @@ import {Navigation} from 'react-native-navigation';
import EmojiPicker from 'app/components/emoji_picker';
import {emptyFunction} from 'app/utils/general';
import {setNavigatorStyles} from 'app/utils/theme';
import {dismissModal, setButtons} from 'app/actions/navigation';
export default class AddReaction extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissModal: PropTypes.func.isRequired,
setButtons: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
closeButton: PropTypes.object,
onEmojiPress: PropTypes.func,
@ -36,7 +33,7 @@ export default class AddReaction extends PureComponent {
constructor(props) {
super(props);
props.actions.setButtons(props.componentId, {
setButtons(props.componentId, {
leftButtons: [{...this.leftButton, icon: props.closeButton}],
});
}
@ -58,7 +55,7 @@ export default class AddReaction extends PureComponent {
}
close = () => {
this.props.actions.dismissModal();
dismissModal();
};
handleEmojiPress = (emoji) => {

View file

@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {dismissModal, setButtons} from 'app/actions/navigation';
import AddReaction from './add_reaction';
function mapStateToProps(state) {
@ -16,13 +13,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissModal,
setButtons,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AddReaction);
export default connect(mapStateToProps)(AddReaction);

View file

@ -13,6 +13,7 @@ import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {DeviceTypes} from 'app/constants';
import {peek} from 'app/actions/navigation';
import LocalConfig from 'assets/config';
@ -26,10 +27,9 @@ const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
export default class ChannelIOS extends ChannelBase {
previewChannel = (passProps, options) => {
const {actions} = this.props;
const screen = 'ChannelPeek';
actions.peek(screen, passProps, options);
peek(screen, passProps, options);
};
optionalProps = {previewChannel: this.previewChannel};

View file

@ -9,7 +9,6 @@ import {
StyleSheet,
View,
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -25,6 +24,11 @@ import PushNotifications from 'app/push_notifications';
import EphemeralStore from 'app/store/ephemeral_store';
import tracker from 'app/utils/time_tracker';
import telemetry from 'app/telemetry';
import {
goToScreen,
showModalOverCurrentContext,
mergeNavigationOptions,
} from 'app/actions/navigation';
import LocalConfig from 'assets/config';
@ -38,9 +42,6 @@ export default class ChannelBase extends PureComponent {
selectDefaultTeam: PropTypes.func.isRequired,
selectInitialChannel: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
peek: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string.isRequired,
@ -68,11 +69,12 @@ export default class ChannelBase extends PureComponent {
this.postTextbox = React.createRef();
this.keyboardTracker = React.createRef();
Navigation.mergeOptions(props.componentId, {
const options = {
layout: {
backgroundColor: props.theme.centerChannelBg,
},
});
};
mergeNavigationOptions(props.componentId, options);
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
@ -113,11 +115,12 @@ export default class ChannelBase extends PureComponent {
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
Navigation.mergeOptions(this.props.componentId, {
const options = {
layout: {
backgroundColor: nextProps.theme.centerChannelBg,
},
});
};
mergeNavigationOptions(this.props.componentId, options);
}
if (nextProps.currentTeamId && this.props.currentTeamId !== nextProps.currentTeamId) {
@ -177,7 +180,7 @@ export default class ChannelBase extends PureComponent {
showTermsOfServiceModal = async () => {
const {intl} = this.context;
const {actions, theme} = this.props;
const {theme} = this.props;
const closeButton = await MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor);
const screen = 'TermsOfService';
const passProps = {closeButton};
@ -196,11 +199,10 @@ export default class ChannelBase extends PureComponent {
},
};
actions.showModalOverCurrentContext(screen, passProps, options);
showModalOverCurrentContext(screen, passProps, options);
};
goToChannelInfo = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'ChannelInfo';
const title = intl.formatMessage({id: 'mobile.routes.channelInfo', defaultMessage: 'Info'});
@ -208,7 +210,7 @@ export default class ChannelBase extends PureComponent {
Keyboard.dismiss();
requestAnimationFrame(() => {
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
});

View file

@ -12,12 +12,12 @@ import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {showSearchModal} from 'app/actions/navigation';
export default class ChannelSearchButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearSearch: PropTypes.func.isRequired,
showSearchModal: PropTypes.func.isRequired,
}).isRequired,
theme: PropTypes.object,
};
@ -27,7 +27,7 @@ export default class ChannelSearchButton extends PureComponent {
Keyboard.dismiss();
await actions.clearSearch();
await actions.showSearchModal();
showSearchModal();
});
render() {

View file

@ -6,15 +6,12 @@ import {connect} from 'react-redux';
import {clearSearch} from 'mattermost-redux/actions/search';
import {showSearchModal} from 'app/actions/navigation';
import ChannelSearchButton from './channel_search_button';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearSearch,
showSearchModal,
}, dispatch),
};
}

View file

@ -19,6 +19,7 @@ import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import telemetry from 'app/telemetry';
import {goToScreen} from 'app/actions/navigation';
let ChannelIntro = null;
let LoadMorePosts = null;
@ -32,7 +33,6 @@ export default class ChannelPostList extends PureComponent {
selectPost: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string.isRequired,
channelRefreshingFailed: PropTypes.bool,
@ -119,7 +119,7 @@ export default class ChannelPostList extends PureComponent {
};
requestAnimationFrame(() => {
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
};

View file

@ -10,7 +10,6 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
import {recordLoadTime} from 'app/actions/views/root';
import {isLandscape} from 'app/selectors/device';
@ -45,7 +44,6 @@ function mapDispatchToProps(dispatch) {
selectPost,
recordLoadTime,
refreshChannelWithRetry,
goToScreen,
}, dispatch),
};
}

View file

@ -20,7 +20,6 @@ import {
import {connection} from 'app/actions/device';
import {recordLoadTime} from 'app/actions/views/root';
import {selectDefaultTeam} from 'app/actions/views/select_team';
import {peek, goToScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import Channel from './channel';
@ -51,9 +50,6 @@ function mapDispatchToProps(dispatch) {
recordLoadTime,
startPeriodicStatusUpdates,
stopPeriodicStatusUpdates,
peek,
goToScreen,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -14,6 +14,7 @@ import {Navigation} from 'react-native-navigation';
import {debounce} from 'mattermost-redux/actions/helpers';
import {General} from 'mattermost-redux/constants';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import Loading from 'app/components/loading';
import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
@ -30,6 +31,7 @@ import {
setNavigatorStyles,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {popTopScreen, setButtons} from 'app/actions/navigation';
export default class ChannelAddMembers extends PureComponent {
static propTypes = {
@ -38,8 +40,6 @@ export default class ChannelAddMembers extends PureComponent {
getProfilesNotInChannel: PropTypes.func.isRequired,
handleAddChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
setButtons: PropTypes.func.isRequired,
popTopScreen: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
currentChannelId: PropTypes.string.isRequired,
@ -82,7 +82,7 @@ export default class ChannelAddMembers extends PureComponent {
showAsAction: 'always',
};
props.actions.setButtons(props.componentId, {
setButtons(props.componentId, {
rightButtons: [this.addButton],
});
}
@ -121,12 +121,12 @@ export default class ChannelAddMembers extends PureComponent {
};
close = () => {
this.props.actions.popTopScreen();
popTopScreen();
};
enableAddOption = (enabled) => {
const {actions, componentId} = this.props;
actions.setButtons(componentId, {
const {componentId} = this.props;
setButtons(componentId, {
rightButtons: [{...this.addButton, enabled}],
});
};

View file

@ -5,6 +5,7 @@ import React from 'react';
import {Preferences} from 'mattermost-redux/constants';
import * as NavigationActions from 'app/actions/navigation';
import {shallowWithIntl} from 'test/intl-test-helper';
import ChannelAddMembers from './channel_add_members';
@ -16,8 +17,6 @@ describe('ChannelAddMembers', () => {
getProfilesNotInChannel: jest.fn().mockResolvedValue({}),
handleAddChannelMembers: jest.fn().mockResolvedValue({}),
searchProfiles: jest.fn().mockResolvedValue({data: []}),
setButtons: jest.fn(),
popTopScreen: jest.fn(),
},
currentChannelId: 'current_channel_id',
currentTeamId: 'current_team_id',
@ -29,15 +28,17 @@ describe('ChannelAddMembers', () => {
};
test('should render without error and call functions on mount', () => {
const setButtons = jest.spyOn(NavigationActions, 'setButtons');
shallowWithIntl(<ChannelAddMembers {...baseProps}/>);
expect(baseProps.actions.getTeamStats).toBeCalledTimes(1);
expect(baseProps.actions.getTeamStats).toBeCalledWith(baseProps.currentTeamId);
const button = {enabled: false, id: 'add-members', text: 'Add', showAsAction: 'always'};
expect(baseProps.actions.setButtons).toBeCalledTimes(2);
expect(baseProps.actions.setButtons.mock.calls[0][0]).toEqual(baseProps.componentId, {rightButtons: [button]});
expect(baseProps.actions.setButtons.mock.calls[1][0]).toEqual(baseProps.componentId, {rightButtons: [button]});
expect(setButtons).toBeCalledTimes(2);
expect(setButtons.mock.calls[0][0]).toEqual(baseProps.componentId, {rightButtons: [button]});
expect(setButtons.mock.calls[1][0]).toEqual(baseProps.componentId, {rightButtons: [button]});
});
test('should match state on clearSearch', () => {
@ -49,11 +50,13 @@ describe('ChannelAddMembers', () => {
wrapper.setState({term: '', searchResults: []});
});
test('should call props.popTopScreen on close', () => {
test('should call popTopScreen on close', () => {
const popTopScreen = jest.spyOn(NavigationActions, 'popTopScreen');
const wrapper = shallowWithIntl(<ChannelAddMembers {...baseProps}/>);
wrapper.instance().close();
expect(baseProps.actions.popTopScreen).toBeCalledTimes(1);
expect(popTopScreen).toBeCalledTimes(1);
});
test('should match state on onProfilesLoaded', () => {

View file

@ -11,7 +11,6 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {setButtons, popTopScreen} from 'app/actions/navigation';
import {handleAddChannelMembers} from 'app/actions/views/channel_add_members';
import {isLandscape} from 'app/selectors/device';
import ChannelAddMembers from './channel_add_members';
@ -37,8 +36,6 @@ function mapDispatchToProps(dispatch) {
getProfilesNotInChannel,
handleAddChannelMembers,
searchProfiles,
setButtons,
popTopScreen,
}, dispatch),
};
}

View file

@ -283,6 +283,7 @@ exports[`channel_info_header should match snapshot 1`] = `
},
}
}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
Object {
@ -718,6 +719,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests 1`] = `
},
}
}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
Object {
@ -1153,6 +1155,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
},
}
}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
Object {
@ -1560,6 +1563,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
},
}
}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
Object {
@ -2016,6 +2020,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
},
}
}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
Object {

View file

@ -17,6 +17,8 @@ import {preventDoubleTap} from 'app/utils/tap';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {goToScreen, popTopScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import pinIcon from 'assets/images/channel_info/pin.png';
import ChannelInfoHeader from './channel_info_header';
@ -41,10 +43,6 @@ export default class ChannelInfo extends PureComponent {
selectPenultimateChannel: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
popTopScreen: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
dismissModal: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}),
componentId: PropTypes.string,
viewArchivedChannels: PropTypes.bool.isRequired,
@ -120,31 +118,30 @@ export default class ChannelInfo extends PureComponent {
actions.setChannelDisplayName('');
}
actions.popTopScreen();
popTopScreen();
};
goToChannelAddMembers = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'ChannelAddMembers';
const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'});
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
goToChannelMembers = preventDoubleTap(() => {
const {actions, canManageUsers} = this.props;
const {canManageUsers} = this.props;
const {intl} = this.context;
const id = canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers');
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
const screen = 'ChannelMembers';
const title = intl.formatMessage({id, defaultMessage});
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
goToPinnedPosts = preventDoubleTap(() => {
const {actions, currentChannel} = this.props;
const {currentChannel} = this.props;
const {formatMessage} = this.context.intl;
const id = t('channel_header.pinnedPosts');
const defaultMessage = 'Pinned Posts';
@ -154,18 +151,17 @@ export default class ChannelInfo extends PureComponent {
currentChannelId: currentChannel.id,
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
handleChannelEdit = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const id = t('mobile.channel_info.edit');
const defaultMessage = 'Edit Channel';
const screen = 'EditChannel';
const title = intl.formatMessage({id, defaultMessage});
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
handleLeave = () => {
@ -324,7 +320,7 @@ export default class ChannelInfo extends PureComponent {
actions.selectFocusedPostId(postId);
this.showingPermalink = true;
actions.showModalOverCurrentContext(screen, passProps, options);
showModalOverCurrentContext(screen, passProps, options);
};
renderViewOrManageMembersRow = () => {
@ -489,7 +485,6 @@ export default class ChannelInfo extends PureComponent {
theme,
isBot,
isLandscape,
actions: {popToRoot},
} = this.props;
const style = getStyleSheet(theme);
@ -530,7 +525,6 @@ export default class ChannelInfo extends PureComponent {
isBot={isBot}
hasGuests={currentChannelGuestCount > 0}
isGroupConstrained={currentChannel.group_constrained}
popToRoot={popToRoot}
/>
}
<View style={style.rowsContainer}>

View file

@ -24,6 +24,7 @@ import BottomSheet from 'app/utils/bottom_sheet';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {popToRoot} from 'app/actions/navigation';
export default class ChannelInfoHeader extends React.PureComponent {
static propTypes = {
@ -42,7 +43,6 @@ export default class ChannelInfoHeader extends React.PureComponent {
hasGuests: PropTypes.bool.isRequired,
isGroupConstrained: PropTypes.bool,
timeZone: PropTypes.string,
popToRoot: PropTypes.func,
};
static contextTypes = {
@ -138,7 +138,6 @@ export default class ChannelInfoHeader extends React.PureComponent {
isBot,
isGroupConstrained,
timeZone,
popToRoot,
} = this.props;
const style = getStyleSheet(theme);

View file

@ -32,13 +32,6 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import {
popTopScreen,
goToScreen,
popToRoot,
dismissModal,
showModalOverCurrentContext,
} from 'app/actions/navigation';
import {
closeDMChannel,
closeGMChannel,
@ -145,11 +138,6 @@ function mapDispatchToProps(dispatch) {
selectPenultimateChannel,
setChannelDisplayName,
handleSelectChannel,
popTopScreen,
goToScreen,
popToRoot,
dismissModal,
showModalOverCurrentContext,
}, dispatch),
};
}

View file

@ -14,6 +14,7 @@ import {Navigation} from 'react-native-navigation';
import {debounce} from 'mattermost-redux/actions/helpers';
import {General} from 'mattermost-redux/constants';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import Loading from 'app/components/loading';
import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
@ -30,6 +31,7 @@ import {
setNavigatorStyles,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {popTopScreen, setButtons} from 'app/actions/navigation';
export default class ChannelMembers extends PureComponent {
static propTypes = {
@ -37,8 +39,6 @@ export default class ChannelMembers extends PureComponent {
getProfilesInChannel: PropTypes.func.isRequired,
handleRemoveChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
setButtons: PropTypes.func.isRequired,
popTopScreen: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
canManageUsers: PropTypes.bool.isRequired,
@ -78,7 +78,7 @@ export default class ChannelMembers extends PureComponent {
};
if (props.canManageUsers) {
props.actions.setButtons(props.componentId, {
setButtons(props.componentId, {
rightButtons: [this.removeButton],
});
}
@ -113,13 +113,13 @@ export default class ChannelMembers extends PureComponent {
};
close = () => {
this.props.actions.popTopScreen();
popTopScreen();
};
enableRemoveOption = (enabled) => {
const {actions, canManageUsers, componentId} = this.props;
const {canManageUsers, componentId} = this.props;
if (canManageUsers) {
actions.setButtons(componentId, {
setButtons(componentId, {
rightButtons: [{...this.removeButton, enabled}],
});
}

View file

@ -19,8 +19,6 @@ describe('ChannelMembers', () => {
getProfilesInChannel: jest.fn().mockImplementation(() => Promise.resolve()),
handleRemoveChannelMembers: jest.fn(),
searchProfiles: jest.fn(),
setButtons: jest.fn(),
popTopScreen: jest.fn(),
},
componentId: 'component-id',
isLandscape: false,

View file

@ -9,7 +9,6 @@ import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selec
import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
import {setButtons, popTopScreen} from 'app/actions/navigation';
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
import {isLandscape} from 'app/selectors/device';
import ChannelMembers from './channel_members';
@ -43,8 +42,6 @@ function mapDispatchToProps(dispatch) {
getProfilesInChannel,
handleRemoveChannelMembers,
searchProfiles,
setButtons,
popTopScreen,
}, dispatch),
};
}

Some files were not shown because too many files have changed in this diff Show more