[CHORE] Update react-navigation to v5 (#2154)

* react-navigation v5 installed

* compiling

* Outside working

* InsideStack compiling

* Switch stack

* Starting room

* RoomView header

* SafeAreaView

* Slide from right stack animation

* stash

* Fix params

* Create channel

* inapp notification

* Custom status

* Add server working

* Refactor appStart

* Attachment

* in-app notification

* AuthLoadingView

* Remove compat

* Navigation

* Outside animations

* Fix new server icon

* block modal

* AttachmentView header

* Remove unnecessary code

* SelectedUsersView header

* StatusView

* CreateDiscussionView

* RoomInfoView

* RoomInfoEditView style

* RoomMembersView

* RoomsListView header

* RoomView header

* Share extension

* getParam

* Focus/blur

* Trying to fix inapp

* Lint

* Simpler app container

* Update libs

* Revert "Simpler app container"

This reverts commit 1e49d80bb4.

* Load messages faster

* Fix safearea on ReactionsModal

* Update safe area to v3

* lint

* Fix transition

* stash - drawer replace working

* stash - modal nav

* RoomActionsView as tablet modal

* RoomStack

* Stop showing RoomView header when there's no room

* Custom Header and different navigation based on stack

* Refactor setHeader

* MasterDetailContext

* RoomView header

* Fix isMasterDetail rule

* KeyCommands kind of working

* Create channel on tablet

* RoomView sCU

* Remove withSplit

* Settings opening as modal

* Settings

* StatusView headerLeft

* Admin panel

* TwoFactor style

* DirectoryView

* ServerDropdown and SortDropdown animations

* ThreadMessagesView

* Navigate to empty RoomView on server switch when in master detail

* ProfileView header

* Fix navigation issues

* Nav to any room info on tablet

* Room info

* Refactoring

* Fix rooms search

* Roomslist commands

* SearchMessagesView close modal

* Key commands

* Fix undefined subscription

* Disallow navigate to focused room

* isFocused state on RoomsListView

* Blur text inputs when focus is lost

* Replace animation

* Default nav theme

* Refactoring

* Always open Attachment with close modal button

* ModalContainer backdrop following themes

* Screen tracking

* Refactor get active route for in-app notification

* Only mark room as focused when in master detail layout

* Lint

* Open modals as fade from bottom on Android

* typo

* Fixing tests

* Fix in-app update

* Fixing goRoom issues

* Refactor stack names

* Fix unreadsCount

* Fix stack

* Fix header animation

* Refactor ShareNavigation

* Refactor navigation theme

* Make sure title is set

* Fix create discussion navigation

* Remove unused variable

* Create discussions from actions fixed

* Layout animation

* Screen lock on share extension

* Unnecessary change

* Admin border

* Set header after state callback

* Fix key commands on outside stack

* Fix back button pressed

* Remove layout animations from Android

* Tweak animations on Android

* Disable swipe gesture to open drawer

* Fix current item on RoomsListView

* Fix add server

* Fix drawer

* Fix broadcast

* LayoutAnimation instead of Transitions

* Fix onboarding back press

* Fix assorted tests

* Create discussion fix

* RoomInfoView header

* Drawer active item
This commit is contained in:
Diego Mello 2020-06-15 11:00:46 -03:00 committed by GitHub
parent 5dcf2212e0
commit 98ed84ba5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
178 changed files with 14168 additions and 15055 deletions

117
app/AppContainer.js Normal file
View File

@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context';
import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import {
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
} from './actions/app';
// Stacks
import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack
import SetUsernameView from './views/SetUsernameView';
import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack';
import { ThemeContext } from './theme';
import { setCurrentScreen } from './utils/log';
// SetUsernameStack
const SetUsername = createStackNavigator();
const SetUsernameStack = () => (
<SetUsername.Navigator screenOptions={defaultHeader}>
<SetUsername.Screen
name='SetUsernameView'
component={SetUsernameView}
/>
</SetUsername.Navigator>
);
// App
const Stack = createStackNavigator();
const App = React.memo(({ root, isMasterDetail }) => {
if (!root) {
return null;
}
const { theme } = React.useContext(ThemeContext);
const navTheme = navigationTheme(theme);
React.useEffect(() => {
const state = Navigation.navigationRef.current.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}, []);
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<NavigationContainer
theme={navTheme}
ref={Navigation.navigationRef}
onStateChange={(state) => {
const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName);
}
Navigation.routeNameRef.current = currentRouteName;
}}
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<>
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null}
</>
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
});
const mapStateToProps = state => ({
root: state.app.root,
isMasterDetail: state.app.isMasterDetail
});
App.propTypes = {
root: PropTypes.string,
isMasterDetail: PropTypes.bool
};
const AppContainer = connect(mapStateToProps)(App);
export default AppContainer;

View File

@ -33,7 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]); export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);

41
app/actions/app.js Normal file
View File

@ -0,0 +1,41 @@
import { APP } from './actionsTypes';
export const ROOT_OUTSIDE = 'outside';
export const ROOT_INSIDE = 'inside';
export const ROOT_LOADING = 'loading';
export const ROOT_NEW_SERVER = 'newServer';
export const ROOT_SET_USERNAME = 'setUsername';
export const ROOT_BACKGROUND = 'background';
export function appStart({ root, ...args }) {
return {
type: APP.START,
root,
...args
};
}
export function appReady() {
return {
type: APP.READY
};
}
export function appInit() {
return {
type: APP.INIT
};
}
export function appInitLocalSettings() {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setMasterDetail(isMasterDetail) {
return {
type: APP.SET_MASTER_DETAIL,
isMasterDetail
};
}

View File

@ -1,41 +0,0 @@
import * as types from '../constants/types';
import { APP } from './actionsTypes';
export function appStart(root, text) {
return {
type: APP.START,
root,
text
};
}
export function appReady() {
return {
type: APP.READY
};
}
export function appInit() {
return {
type: APP.INIT
};
}
export function appInitLocalSettings() {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setCurrentServer(server) {
return {
type: types.SET_CURRENT_SERVER,
payload: server
};
}
export function login() {
return {
type: 'LOGIN'
};
}

View File

@ -44,9 +44,10 @@ export function serverFailure(err) {
}; };
} }
export function serverInitAdd() { export function serverInitAdd(previousServer) {
return { return {
type: SERVER.INIT_ADD type: SERVER.INIT_ADD,
previousServer
}; };
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import { constants } from 'react-native-keycommands'; import KeyCommands, { constants } from 'react-native-keycommands';
import I18n from './i18n'; import I18n from './i18n';
@ -17,7 +17,7 @@ const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n';
const KEY_SEND_MESSAGE = '\r'; const KEY_SEND_MESSAGE = '\r';
const KEY_SELECT = '123456789'; const KEY_SELECT = '123456789';
export const defaultCommands = [ const keyCommands = [
{ {
// Focus messageBox // Focus messageBox
input: KEY_TYPING, input: KEY_TYPING,
@ -29,10 +29,7 @@ export const defaultCommands = [
input: KEY_SEND_MESSAGE, input: KEY_SEND_MESSAGE,
modifierFlags: 0, modifierFlags: 0,
discoverabilityTitle: I18n.t('Send') discoverabilityTitle: I18n.t('Send')
} },
];
export const keyCommands = [
{ {
// Open Preferences Modal // Open Preferences Modal
input: KEY_PREFERENCES, input: KEY_PREFERENCES,
@ -139,6 +136,10 @@ export const keyCommands = [
}))) })))
]; ];
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
export const deleteKeyCommands = () => KeyCommands.deleteKeyCommands(keyCommands);
export const KEY_COMMAND = 'KEY_COMMAND'; export const KEY_COMMAND = 'KEY_COMMAND';
export const commandHandle = (event, key, flags = []) => { export const commandHandle = (event, key, flags = []) => {

View File

@ -1,4 +1,3 @@
export const MAX_SIDEBAR_WIDTH = 321; export const MAX_SIDEBAR_WIDTH = 321;
export const MAX_CONTENT_WIDTH = '90%';
export const MAX_SCREEN_CONTENT_WIDTH = '50%'; export const MAX_SCREEN_CONTENT_WIDTH = '50%';
export const MIN_WIDTH_SPLIT_LAYOUT = 700; export const MIN_WIDTH_MASTER_DETAIL_LAYOUT = 700;

View File

@ -1,4 +0,0 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ADD_SETTINGS = 'ADD_SETTINGS';
export const CLEAR_SETTINGS = 'CLEAR_SETTINGS';

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-navigation';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -10,6 +9,7 @@ import KeyboardView from '../presentation/KeyboardView';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
import AppVersion from './AppVersion'; import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollView: { scrollView: {
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme, testID }) => (
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}> <ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}> <SafeAreaView testID={testID} theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
{children} {children}
<AppVersion theme={theme} /> <AppVersion theme={theme} />
</SafeAreaView> </SafeAreaView>

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-native-safe-area-context';
import { View, StyleSheet } from 'react-native';
import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation';
import { isIOS } from '../../utils/deviceInfo';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56;
const styles = StyleSheet.create({
container: {
height: headerHeight,
flexDirection: 'row',
justifyContent: 'center',
elevation: 4
}
});
const Header = ({
theme, headerLeft, headerTitle, headerRight
}) => (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
{headerTitle ? headerTitle() : null}
{headerRight ? headerRight() : null}
</View>
</SafeAreaView>
);
Header.propTypes = {
theme: PropTypes.string,
headerLeft: PropTypes.element,
headerTitle: PropTypes.element,
headerRight: PropTypes.element
};
export default Header;

View File

@ -5,7 +5,6 @@ import {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { withNavigation } from 'react-navigation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -361,4 +360,4 @@ const mapDispatchToProps = dispatch => ({
loginRequest: params => dispatch(loginRequestAction(params)) loginRequest: params => dispatch(loginRequestAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withNavigation(LoginServices))); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginServices));

View File

@ -32,7 +32,8 @@ class MessageActions extends React.Component {
Message_AllowEditing_BlockEditInMinutes: PropTypes.number, Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
Message_AllowPinning: PropTypes.bool, Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool, Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool Message_Read_Receipt_Store_Users: PropTypes.bool,
isMasterDetail: PropTypes.bool
}; };
constructor(props) { constructor(props) {
@ -254,7 +255,7 @@ class MessageActions extends React.Component {
} }
handleUnread = async() => { handleUnread = async() => {
const { message, room } = this.props; const { message, room, isMasterDetail } = this.props;
const { id: messageId, ts } = message; const { id: messageId, ts } = message;
const { rid } = room; const { rid } = room;
try { try {
@ -270,8 +271,12 @@ class MessageActions extends React.Component {
// do nothing // do nothing
} }
}); });
if (isMasterDetail) {
Navigation.replace('RoomView');
} else {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
} }
}
} catch (e) { } catch (e) {
log(e); log(e);
} }
@ -376,8 +381,13 @@ class MessageActions extends React.Component {
} }
handleCreateDiscussion = () => { handleCreateDiscussion = () => {
const { message, room: channel } = this.props; const { message, room: channel, isMasterDetail } = this.props;
Navigation.navigate('CreateDiscussionView', { message, channel }); const params = { message, channel, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
@ -450,7 +460,8 @@ const mapStateToProps = state => ({
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring, Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(MessageActions); export default connect(mapStateToProps)(MessageActions);

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, SafeAreaView, PermissionsAndroid, Text View, PermissionsAndroid, Text
} from 'react-native'; } from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio'; import { AudioRecorder, AudioUtils } from 'react-native-audio';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
@ -13,6 +13,7 @@ import I18n from '../../i18n';
import { isIOS, isAndroid } from '../../utils/deviceInfo'; import { isIOS, isAndroid } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import SafeAreaView from '../SafeAreaView';
export const _formatTime = function(seconds) { export const _formatTime = function(seconds) {
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60);
@ -134,6 +135,7 @@ export default class extends React.PureComponent {
return ( return (
<SafeAreaView <SafeAreaView
testID='messagebox-recording' testID='messagebox-recording'
theme={theme}
style={[ style={[
styles.textBox, styles.textBox,
{ borderTopColor: themes[theme].borderColor } { borderTopColor: themes[theme].borderColor }

View File

@ -15,7 +15,6 @@ import { isIOS } from '../../utils/deviceInfo';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
modal: { modal: {
@ -91,7 +90,7 @@ class UploadModal extends Component {
submit: PropTypes.func, submit: PropTypes.func,
window: PropTypes.object, window: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool isMasterDetail: PropTypes.bool
} }
state = { state = {
@ -113,16 +112,11 @@ class UploadModal extends Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { name, description, file } = this.state; const { name, description, file } = this.state;
const { const { window, isVisible, theme } = this.props;
window, isVisible, split, theme
} = this.props;
if (nextState.name !== name) { if (nextState.name !== name) {
return true; return true;
} }
if (nextProps.split !== split) {
return true;
}
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -194,9 +188,9 @@ class UploadModal extends Component {
} }
renderPreview() { renderPreview() {
const { file, split, theme } = this.props; const { file, theme, isMasterDetail } = this.props;
if (file.mime && file.mime.match(/image/)) { if (file.mime && file.mime.match(/image/)) {
return (<Image source={{ isStatic: true, uri: file.path }} style={[styles.image, split && styles.bigPreview]} />); return (<Image source={{ isStatic: true, uri: file.path }} style={[styles.image, isMasterDetail && styles.bigPreview]} />);
} }
if (file.mime && file.mime.match(/video/)) { if (file.mime && file.mime.match(/video/)) {
return ( return (
@ -210,7 +204,7 @@ class UploadModal extends Component {
render() { render() {
const { const {
window: { width }, isVisible, close, split, theme window: { width }, isVisible, close, isMasterDetail, theme
} = this.props; } = this.props;
const { name, description } = this.state; const { name, description } = this.state;
return ( return (
@ -225,7 +219,7 @@ class UploadModal extends Component {
hideModalContentWhileAnimating hideModalContentWhileAnimating
avoidKeyboard avoidKeyboard
> >
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && [sharedStyles.modal, sharedStyles.modalFormSheet]]}> <View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, isMasterDetail && sharedStyles.modalFormSheet]}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text> <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
</View> </View>
@ -252,4 +246,4 @@ class UploadModal extends Component {
} }
} }
export default responsive(withTheme(withSplit(UploadModal))); export default responsive(withTheme(UploadModal));

View File

@ -95,6 +95,7 @@ class MessageBox extends Component {
typing: PropTypes.func, typing: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
replyCancel: PropTypes.func, replyCancel: PropTypes.func,
isMasterDetail: PropTypes.bool,
navigation: PropTypes.object navigation: PropTypes.object
} }
@ -183,11 +184,14 @@ class MessageBox extends Component {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
} }
this.didFocusListener = navigation.addListener('didFocus', () => { this.unsubscribeFocus = navigation.addListener('focus', () => {
if (this.tracking && this.tracking.resetTracking) { if (this.tracking && this.tracking.resetTracking) {
this.tracking.resetTracking(); this.tracking.resetTracking();
} }
}); });
this.unsubscribeBlur = navigation.addListener('blur', () => {
this.component?.blur();
});
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
@ -268,8 +272,11 @@ class MessageBox extends Component {
if (this.getSlashCommands && this.getSlashCommands.stop) { if (this.getSlashCommands && this.getSlashCommands.stop) {
this.getSlashCommands.stop(); this.getSlashCommands.stop();
} }
if (this.didFocusListener && this.didFocusListener.remove) { if (this.unsubscribeFocus) {
this.didFocusListener.remove(); this.unsubscribeFocus();
}
if (this.unsubscribeBlur) {
this.unsubscribeBlur();
} }
if (isTablet) { if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands); EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
@ -592,7 +599,13 @@ class MessageBox extends Component {
} }
createDiscussion = () => { createDiscussion = () => {
Navigation.navigate('CreateDiscussionView', { channel: this.room }); const { isMasterDetail } = this.props;
const params = { channel: this.room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
} }
showUploadModal = (file) => { showUploadModal = (file) => {
@ -875,7 +888,9 @@ class MessageBox extends Component {
render() { render() {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { showEmojiKeyboard, file } = this.state; const { showEmojiKeyboard, file } = this.state;
const { user, baseUrl, theme } = this.props; const {
user, baseUrl, theme, isMasterDetail
} = this.props;
return ( return (
<MessageboxContext.Provider <MessageboxContext.Provider
value={{ value={{
@ -903,6 +918,7 @@ class MessageBox extends Component {
file={file} file={file}
close={() => this.setState({ file: {} })} close={() => this.setState({ file: {} })}
submit={this.sendMediaMessage} submit={this.sendMediaMessage}
isMasterDetail={isMasterDetail}
/> />
</MessageboxContext.Provider> </MessageboxContext.Provider>
); );
@ -910,6 +926,7 @@ class MessageBox extends Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server, baseUrl: state.server.server,
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled,
user: getUserSelector(state), user: getUserSelector(state),

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { import {
View, Text, FlatList, StyleSheet, SafeAreaView View, Text, FlatList, StyleSheet
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
@ -12,8 +12,12 @@ import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeArea: {
backgroundColor: 'transparent'
},
titleContainer: { titleContainer: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 10 paddingVertical: 10
@ -95,7 +99,7 @@ const ModalContent = React.memo(({
}) => { }) => {
if (message && message.reactions) { if (message && message.reactions) {
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView theme={props.theme} style={styles.safeArea}>
<Touchable onPress={onClose}> <Touchable onPress={onClose}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<CustomIcon <CustomIcon

View File

@ -0,0 +1,34 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { SafeAreaView as SafeAreaContext } from 'react-native-safe-area-context';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
view: {
flex: 1
}
});
const SafeAreaView = React.memo(({
style, children, testID, theme, vertical = true, ...props
}) => (
<SafeAreaContext
style={[styles.view, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
edges={vertical ? ['right', 'left'] : undefined}
testID={testID}
{...props}
>
{children}
</SafeAreaContext>
));
SafeAreaView.propTypes = {
testID: PropTypes.string,
theme: PropTypes.string,
vertical: PropTypes.bool,
style: PropTypes.object,
children: PropTypes.element
};
export default SafeAreaView;

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme';
const StatusBar = React.memo(({ theme }) => { const StatusBar = React.memo(({ theme }) => {
let barStyle = 'light-content'; let barStyle = 'light-content';
@ -18,4 +17,4 @@ StatusBar.propTypes = {
theme: PropTypes.string theme: PropTypes.string
}; };
export default withTheme(StatusBar); export default StatusBar;

View File

@ -5,12 +5,12 @@ import PropTypes from 'prop-types';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect'; import useDeepCompareEffect from 'use-deep-compare-effect';
import { connect } from 'react-redux';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Button from '../Button'; import Button from '../Button';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
@ -36,7 +36,7 @@ const methods = {
} }
}; };
const TwoFactor = React.memo(({ theme, split }) => { const TwoFactor = React.memo(({ theme, isMasterDetail }) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [data, setData] = useState({}); const [data, setData] = useState({});
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@ -93,7 +93,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
hideModalContentWhileAnimating hideModalContentWhileAnimating
> >
<View style={styles.container} testID='two-factor'> <View style={styles.container} testID='two-factor'>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.content, isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text> <Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null} {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput <TextInput
@ -134,7 +134,11 @@ const TwoFactor = React.memo(({ theme, split }) => {
}); });
TwoFactor.propTypes = { TwoFactor.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool isMasterDetail: PropTypes.bool
}; };
export default withSplit(withTheme(TwoFactor)); const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(withTheme(TwoFactor));

View File

@ -37,5 +37,8 @@ export default StyleSheet.create({
buttonContainer: { buttonContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between'
},
tablet: {
height: undefined
} }
}); });

View File

@ -15,7 +15,6 @@ import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split';
import MessageContext from './Context'; import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
@ -98,7 +97,6 @@ class MessageAudio extends React.Component {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
} }
@ -138,7 +136,7 @@ class MessageAudio extends React.Component {
const { const {
currentTime, duration, paused, loading currentTime, duration, paused, loading
} = this.state; } = this.state;
const { file, split, theme } = this.props; const { file, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -154,9 +152,6 @@ class MessageAudio extends React.Component {
if (!equal(nextProps.file, file)) { if (!equal(nextProps.file, file)) {
return true; return true;
} }
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) { if (nextState.loading !== loading) {
return true; return true;
} }
@ -248,9 +243,7 @@ class MessageAudio extends React.Component {
const { const {
loading, paused, currentTime, duration loading, paused, currentTime, duration
} = this.state; } = this.state;
const { const { file, getCustomEmoji, theme } = this.props;
file, getCustomEmoji, split, theme
} = this.props;
const { description } = file; const { description } = file;
const { baseUrl, user } = this.context; const { baseUrl, user } = this.context;
@ -263,8 +256,7 @@ class MessageAudio extends React.Component {
<View <View
style={[ style={[
styles.audioContainer, styles.audioContainer,
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }, { backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
split && sharedStyles.tabletContent
]} ]}
> >
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} /> <Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
@ -289,4 +281,4 @@ class MessageAudio extends React.Component {
} }
} }
export default withSplit(MessageAudio); export default MessageAudio;

View File

@ -10,19 +10,17 @@ import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context'; import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
const Button = React.memo(({ const Button = React.memo(({
children, onPress, split, theme children, onPress, theme
}) => ( }) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
style={[styles.imageContainer, split && sharedStyles.tabletContent]} style={styles.imageContainer}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
{children} {children}
@ -42,7 +40,7 @@ export const MessageImage = React.memo(({ img, theme }) => (
)); ));
const ImageContainer = React.memo(({ const ImageContainer = React.memo(({
file, imageUrl, showAttachment, getCustomEmoji, split, theme file, imageUrl, showAttachment, getCustomEmoji, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
@ -54,7 +52,7 @@ const ImageContainer = React.memo(({
if (file.description) { if (file.description) {
return ( return (
<Button split={split} theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<View> <View>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} /> <Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
@ -64,19 +62,18 @@ const ImageContainer = React.memo(({
} }
return ( return (
<Button split={split} theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
</Button> </Button>
); );
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme);
ImageContainer.propTypes = { ImageContainer.propTypes = {
file: PropTypes.object, file: PropTypes.object,
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func
split: PropTypes.bool
}; };
ImageContainer.displayName = 'MessageImageContainer'; ImageContainer.displayName = 'MessageImageContainer';
@ -89,9 +86,8 @@ ImageContainer.displayName = 'MessageImage';
Button.propTypes = { Button.propTypes = {
children: PropTypes.node, children: PropTypes.node,
onPress: PropTypes.func, onPress: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string
split: PropTypes.bool
}; };
ImageContainer.displayName = 'MessageButton'; ImageContainer.displayName = 'MessageButton';
export default withSplit(ImageContainer); export default ImageContainer;

View File

@ -9,7 +9,6 @@ import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withSplit } from '../../split';
import MessageContext from './Context'; import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -126,7 +125,7 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({ const Reply = React.memo(({
attachment, timeFormat, index, getCustomEmoji, split, theme attachment, timeFormat, index, getCustomEmoji, theme
}) => { }) => {
if (!attachment) { if (!attachment) {
return null; return null;
@ -156,8 +155,7 @@ const Reply = React.memo(({
{ {
backgroundColor: themes[theme].chatComponentBackground, backgroundColor: themes[theme].chatComponentBackground,
borderColor: themes[theme].borderColor borderColor: themes[theme].borderColor
}, }
split && sharedStyles.tabletContent
]} ]}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
@ -173,15 +171,14 @@ const Reply = React.memo(({
</View> </View>
</Touchable> </Touchable>
); );
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment) && prevProps.theme === nextProps.theme);
Reply.propTypes = { Reply.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
index: PropTypes.number, index: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func
split: PropTypes.bool
}; };
Reply.displayName = 'MessageReply'; Reply.displayName = 'MessageReply';
@ -205,4 +202,4 @@ Fields.propTypes = {
}; };
Fields.displayName = 'MessageReplyFields'; Fields.displayName = 'MessageReplyFields';
export default withSplit(Reply); export default Reply;

View File

@ -11,7 +11,6 @@ import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -80,9 +79,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
return true; return true;
}); });
const Url = React.memo(({ const Url = React.memo(({ url, index, theme }) => {
url, index, split, theme
}) => {
if (!url) { if (!url) {
return null; return null;
} }
@ -105,8 +102,7 @@ const Url = React.memo(({
{ {
backgroundColor: themes[theme].chatComponentBackground, backgroundColor: themes[theme].chatComponentBackground,
borderColor: themes[theme].borderColor borderColor: themes[theme].borderColor
}, }
split && sharedStyles.tabletContent
]} ]}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
@ -116,19 +112,17 @@ const Url = React.memo(({
</> </>
</Touchable> </Touchable>
); );
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.theme === newProps.theme);
const Urls = React.memo(({ const Urls = React.memo(({ urls, theme }) => {
urls, split, theme
}) => {
if (!urls || urls.length === 0) { if (!urls || urls.length === 0) {
return null; return null;
} }
return urls.map((url, index) => ( return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} split={split} theme={theme} /> <Url url={url} key={url.url} index={index} theme={theme} />
)); ));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.theme === newProps.theme);
UrlImage.propTypes = { UrlImage.propTypes = {
image: PropTypes.string image: PropTypes.string
@ -145,16 +139,14 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = { Url.propTypes = {
url: PropTypes.object.isRequired, url: PropTypes.object.isRequired,
index: PropTypes.number, index: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string
split: PropTypes.bool
}; };
Url.displayName = 'MessageUrl'; Url.displayName = 'MessageUrl';
Urls.propTypes = { Urls.propTypes = {
urls: PropTypes.array, urls: PropTypes.array,
theme: PropTypes.string, theme: PropTypes.string
split: PropTypes.bool
}; };
Urls.displayName = 'MessageUrls'; Urls.displayName = 'MessageUrls';
export default withTheme(withSplit(Urls)); export default withTheme(Urls);

View File

@ -6,11 +6,10 @@ import isEqual from 'deep-equal';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context'; import MessageContext from './Context';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
@ -46,7 +45,7 @@ const Video = React.memo(({
<> <>
<Touchable <Touchable
onPress={onPress} onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }, isTablet && sharedStyles.tabletContent]} style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
<CustomIcon <CustomIcon

View File

@ -1,16 +1,10 @@
import React from 'react'; import React from 'react';
import { import { Linking, Dimensions } from 'react-native';
View, Linking, BackHandler, ScrollView
} from 'react-native';
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createDrawerNavigator } from 'react-navigation-drawer';
import { AppearanceProvider } from 'react-native-appearance'; import { AppearanceProvider } from 'react-native-appearance';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import Modal from 'react-native-modal'; import { KeyCommandsEmitter } from 'react-native-keycommands';
import KeyCommands, { KeyCommandsEmitter } from 'react-native-keycommands'; import RNScreens from 'react-native-screens';
import { import {
defaultTheme, defaultTheme,
@ -19,40 +13,25 @@ import {
unsubscribeTheme unsubscribeTheme
} from './utils/theme'; } from './utils/theme';
import EventEmitter from './utils/events'; import EventEmitter from './utils/events';
import { appInit, appInitLocalSettings } from './actions'; import { appInit, appInitLocalSettings, setMasterDetail as setMasterDetailAction } from './actions/app';
import { deepLinkingOpen } from './actions/deepLinking'; import { deepLinkingOpen } from './actions/deepLinking';
import Navigation from './lib/Navigation';
import Sidebar from './views/SidebarView';
import parseQuery from './lib/methods/helpers/parseQuery'; import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications, onNotification } from './notifications/push'; import { initializePushNotifications, onNotification } from './notifications/push';
import store from './lib/createStore'; import store from './lib/createStore';
import NotificationBadge from './notifications/inApp';
import {
defaultHeader, onNavigationStateChange, cardStyle, getActiveRouteName
} from './utils/navigation';
import { loggerConfig, analytics } from './utils/log'; import { loggerConfig, analytics } from './utils/log';
import Toast from './containers/Toast';
import { ThemeContext } from './theme'; import { ThemeContext } from './theme';
import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat';
import { MIN_WIDTH_SPLIT_LAYOUT } from './constants/tablet'; import { MIN_WIDTH_MASTER_DETAIL_LAYOUT } from './constants/tablet';
import { import {
isTablet, isSplited, isIOS, setWidth, supportSystemTheme, isAndroid isTablet, supportSystemTheme
} from './utils/deviceInfo'; } from './utils/deviceInfo';
import { KEY_COMMAND } from './commands'; import { KEY_COMMAND } from './commands';
import Tablet, { initTabletNav } from './tablet'; import AppContainer from './AppContainer';
import sharedStyles from './views/Styles';
import { SplitContext } from './split';
import TwoFactor from './containers/TwoFactor'; import TwoFactor from './containers/TwoFactor';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
import ScreenLockedView from './views/ScreenLockedView'; import ScreenLockedView from './views/ScreenLockedView';
import ChangePasscodeView from './views/ChangePasscodeView'; import ChangePasscodeView from './views/ChangePasscodeView';
if (isIOS) { RNScreens.enableScreens();
const RNScreens = require('react-native-screens');
RNScreens.useScreens();
}
const parseDeepLinking = (url) => { const parseDeepLinking = (url) => {
if (url) { if (url) {
@ -68,543 +47,12 @@ const parseDeepLinking = (url) => {
return null; return null;
}; };
// Outside
const OutsideStack = createStackNavigator({
OnboardingView: {
getScreen: () => require('./views/OnboardingView').default,
header: null
},
NewServerView: {
getScreen: () => require('./views/NewServerView').default
},
WorkspaceView: {
getScreen: () => require('./views/WorkspaceView').default
},
LoginView: {
getScreen: () => require('./views/LoginView').default
},
ForgotPasswordView: {
getScreen: () => require('./views/ForgotPasswordView').default
},
RegisterView: {
getScreen: () => require('./views/RegisterView').default
},
LegalView: {
getScreen: () => require('./views/LegalView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const AuthenticationWebViewStack = createStackNavigator({
AuthenticationWebView: {
getScreen: () => require('./views/AuthenticationWebView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const OutsideStackModal = createStackNavigator({
OutsideStack,
AuthenticationWebViewStack
},
{
mode: 'modal',
headerMode: 'none',
cardStyle
});
const RoomRoutes = {
RoomView,
ThreadMessagesView: {
getScreen: () => require('./views/ThreadMessagesView').default
},
MarkdownTableView: {
getScreen: () => require('./views/MarkdownTableView').default
},
ReadReceiptsView: {
getScreen: () => require('./views/ReadReceiptView').default
}
};
// Inside
const ChatsStack = createStackNavigator({
RoomsListView,
RoomActionsView: {
getScreen: () => require('./views/RoomActionsView').default
},
RoomInfoView: {
getScreen: () => require('./views/RoomInfoView').default
},
RoomInfoEditView: {
getScreen: () => require('./views/RoomInfoEditView').default
},
RoomMembersView: {
getScreen: () => require('./views/RoomMembersView').default
},
SearchMessagesView: {
getScreen: () => require('./views/SearchMessagesView').default
},
SelectedUsersView: {
getScreen: () => require('./views/SelectedUsersView').default
},
InviteUsersView: {
getScreen: () => require('./views/InviteUsersView').default
},
InviteUsersEditView: {
getScreen: () => require('./views/InviteUsersEditView').default
},
MessagesView: {
getScreen: () => require('./views/MessagesView').default
},
AutoTranslateView: {
getScreen: () => require('./views/AutoTranslateView').default
},
DirectoryView: {
getScreen: () => require('./views/DirectoryView').default
},
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
},
VisitorNavigationView: {
getScreen: () => require('./views/VisitorNavigationView').default
},
ForwardLivechatView: {
getScreen: () => require('./views/ForwardLivechatView').default
},
LivechatEditView: {
getScreen: () => require('./views/LivechatEditView').default
},
PickerView: {
getScreen: () => require('./views/PickerView').default
},
...RoomRoutes
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
// Inside
const RoomStack = createStackNavigator({
...RoomRoutes
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
ChatsStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0 || isSplited()) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const ProfileStack = createStackNavigator({
ProfileView: {
getScreen: () => require('./views/ProfileView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
ProfileStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const SettingsStack = createStackNavigator({
SettingsView: {
getScreen: () => require('./views/SettingsView').default
},
LanguageView: {
getScreen: () => require('./views/LanguageView').default
},
ThemeView: {
getScreen: () => require('./views/ThemeView').default
},
DefaultBrowserView: {
getScreen: () => require('./views/DefaultBrowserView').default
},
ScreenLockConfigView: {
getScreen: () => require('./views/ScreenLockConfigView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const AdminPanelStack = createStackNavigator({
AdminPanelView: {
getScreen: () => require('./views/AdminPanelView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
SettingsStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const ChatsDrawer = createDrawerNavigator({
ChatsStack,
ProfileStack,
SettingsStack,
AdminPanelStack
}, {
contentComponent: Sidebar,
overlayColor: '#00000090'
});
const NewMessageStack = createStackNavigator({
NewMessageView: {
getScreen: () => require('./views/NewMessageView').default
},
SelectedUsersViewCreateChannel: {
getScreen: () => require('./views/SelectedUsersView').default
},
CreateChannelView: {
getScreen: () => require('./views/CreateChannelView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const AttachmentStack = createStackNavigator({
AttachmentView: {
getScreen: () => require('./views/AttachmentView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const ModalBlockStack = createStackNavigator({
ModalBlockView: {
getScreen: () => require('./views/ModalBlockView').default
}
}, {
mode: 'modal',
defaultNavigationOptions: defaultHeader,
cardStyle
});
const CreateDiscussionStack = createStackNavigator({
CreateDiscussionView: {
getScreen: () => require('./views/CreateDiscussionView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const StatusStack = createStackNavigator({
StatusView: {
getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const InsideStackModal = createStackNavigator({
Main: ChatsDrawer,
NewMessageStack,
AttachmentStack,
ModalBlockStack,
StatusStack,
CreateDiscussionStack,
JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default
}
},
{
mode: 'modal',
headerMode: 'none',
cardStyle
});
const SetUsernameStack = createStackNavigator({
SetUsernameView: {
getScreen: () => require('./views/SetUsernameView').default
}
},
{
cardStyle
});
class CustomInsideStack extends React.Component {
static router = InsideStackModal.router;
static propTypes = {
navigation: PropTypes.object,
screenProps: PropTypes.object
}
render() {
const { navigation, screenProps } = this.props;
return (
<>
<InsideStackModal navigation={navigation} screenProps={screenProps} />
{ !isTablet ? <NotificationBadge navigation={navigation} /> : null }
{ !isTablet ? <Toast /> : null }
</>
);
}
}
class CustomRoomStack extends React.Component {
static router = RoomStack.router;
static propTypes = {
navigation: PropTypes.object,
screenProps: PropTypes.object
}
render() {
const { navigation, screenProps } = this.props;
return (
<>
<RoomStack navigation={navigation} screenProps={screenProps} />
<Toast />
</>
);
}
}
const MessagesStack = createStackNavigator({
NewMessageView: {
getScreen: () => require('./views/NewMessageView').default
},
SelectedUsersViewCreateChannel: {
getScreen: () => require('./views/SelectedUsersView').default
},
CreateChannelView: {
getScreen: () => require('./views/CreateChannelView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const DirectoryStack = createStackNavigator({
DirectoryView: {
getScreen: () => require('./views/DirectoryView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const SidebarStack = createStackNavigator({
SettingsView: {
getScreen: () => require('./views/SettingsView').default
},
ProfileView: {
getScreen: () => require('./views/ProfileView').default
},
AdminPanelView: {
getScreen: () => require('./views/AdminPanelView').default
},
StatusView: {
getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const RoomActionsStack = createStackNavigator({
RoomActionsView: {
getScreen: () => require('./views/RoomActionsView').default
},
RoomInfoView: {
getScreen: () => require('./views/RoomInfoView').default
},
RoomInfoEditView: {
getScreen: () => require('./views/RoomInfoEditView').default
},
RoomMembersView: {
getScreen: () => require('./views/RoomMembersView').default
},
SearchMessagesView: {
getScreen: () => require('./views/SearchMessagesView').default
},
SelectedUsersView: {
getScreen: () => require('./views/SelectedUsersView').default
},
MessagesView: {
getScreen: () => require('./views/MessagesView').default
},
AutoTranslateView: {
getScreen: () => require('./views/AutoTranslateView').default
},
ReadReceiptsView: {
getScreen: () => require('./views/ReadReceiptView').default
},
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
},
AttachmentView: {
getScreen: () => require('./views/AttachmentView').default
},
PickerView: {
getScreen: () => require('./views/PickerView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const ModalSwitch = createSwitchNavigator({
MessagesStack,
DirectoryStack,
SidebarStack,
RoomActionsStack,
SettingsStack,
ModalBlockStack,
CreateDiscussionStack,
AuthLoading: () => null
},
{
initialRouteName: 'AuthLoading'
});
class CustomModalStack extends React.Component {
static router = ModalSwitch.router;
static propTypes = {
navigation: PropTypes.object,
showModal: PropTypes.bool,
closeModal: PropTypes.func,
screenProps: PropTypes.object
}
componentDidMount() {
this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.closeModal);
}
componentWillUnmount() {
this.backHandler.remove();
}
closeModal = () => {
const { closeModal, navigation } = this.props;
const { state } = navigation;
if (state && state.routes[state.index] && state.routes[state.index].index === 0) {
closeModal();
return true;
}
if (state && state.routes[state.index] && state.routes[state.index].routes && state.routes[state.index].routes.length > 1) {
navigation.goBack();
}
return false;
}
render() {
const {
navigation, showModal, closeModal, screenProps
} = this.props;
const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && !pageSheet && {
style: { marginBottom: 0 }
};
let content = (
<View style={[sharedStyles.modal, pageSheet ? sharedStyles.modalPageSheet : sharedStyles.modalFormSheet]}>
<ModalSwitch navigation={navigation} screenProps={{ ...screenProps, closeModal: this.closeModal }} />
</View>
);
if (isAndroid && !pageSheet) {
content = (
<ScrollView overScrollMode='never'>
{content}
</ScrollView>
);
}
return (
<Modal
useNativeDriver
coverScreen={false}
isVisible={showModal}
onBackdropPress={closeModal}
hideModalContentWhileAnimating
avoidKeyboard
{...androidProps}
>
{content}
</Modal>
);
}
}
class CustomNotificationStack extends React.Component {
static router = InsideStackModal.router;
static propTypes = {
navigation: PropTypes.object,
screenProps: PropTypes.object
}
render() {
const { navigation, screenProps } = this.props;
return <NotificationBadge navigation={navigation} screenProps={screenProps} />;
}
}
export const App = createAppContainer(createSwitchNavigator(
{
OutsideStack: OutsideStackModal,
InsideStack: CustomInsideStack,
AuthLoading: {
getScreen: () => require('./views/AuthLoadingView').default
},
SetUsernameStack
},
{
initialRouteName: 'AuthLoading'
}
));
export const RoomContainer = createAppContainer(CustomRoomStack);
export const ModalContainer = createAppContainer(CustomModalStack);
export const NotificationContainer = createAppContainer(CustomNotificationStack);
export default class Root extends React.Component { export default class Root extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.init(); this.init();
this.initCrashReport(); this.initCrashReport();
this.state = { this.state = {
split: false,
inside: false,
showModal: false,
theme: defaultTheme(), theme: defaultTheme(),
themePreferences: { themePreferences: {
currentTheme: supportSystemTheme() ? 'automatic' : 'light', currentTheme: supportSystemTheme() ? 'automatic' : 'light',
@ -625,22 +73,12 @@ export default class Root extends React.Component {
} }
}); });
}, 5000); }, 5000);
} Dimensions.addEventListener('change', this.onDimensionsChange);
// eslint-disable-next-line no-unused-vars
componentDidUpdate(_, prevState) {
if (isTablet) {
const { split, inside } = this.state;
if (inside && split !== prevState.split) {
// Reset app on split mode changes
Navigation.navigate('RoomsListView');
this.closeModal();
}
}
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this.listenerTimeout); clearTimeout(this.listenerTimeout);
Dimensions.removeEventListener('change', this.onDimensionsChange);
unsubscribeTheme(); unsubscribeTheme();
@ -663,6 +101,22 @@ export default class Root extends React.Component {
} }
} }
getMasterDetail = (width) => {
if (!isTablet) {
return false;
}
return width > MIN_WIDTH_MASTER_DETAIL_LAYOUT;
}
setMasterDetail = (width) => {
const isMasterDetail = this.getMasterDetail(width);
store.dispatch(setMasterDetailAction(isMasterDetail));
};
onDimensionsChange = ({ window: { width } }) => {
this.setMasterDetail(width);
}
setTheme = (newTheme = {}) => { setTheme = (newTheme = {}) => {
// change theme state // change theme state
this.setState(prevState => newThemeState(prevState, newTheme), () => { this.setState(prevState => newThemeState(prevState, newTheme), () => {
@ -672,12 +126,14 @@ export default class Root extends React.Component {
}); });
} }
initTablet = async() => { initTablet = () => {
initTabletNav(args => this.setState(args)); const { width } = Dimensions.get('window');
await KeyCommands.setKeyCommands([]); this.setMasterDetail(width);
this.onKeyCommands = KeyCommandsEmitter.addListener( this.onKeyCommands = KeyCommandsEmitter.addListener(
'onKeyCommand', 'onKeyCommand',
command => EventEmitter.emit(KEY_COMMAND, { event: command }) (command) => {
EventEmitter.emit(KEY_COMMAND, { event: command });
}
); );
} }
@ -692,45 +148,8 @@ export default class Root extends React.Component {
}); });
} }
onLayout = ({ nativeEvent: { layout: { width } } }) => (isTablet ? this.setSplit(width) : null);
setSplit = (width) => {
this.setState({ split: width > MIN_WIDTH_SPLIT_LAYOUT });
setWidth(width);
}
closeModal = () => this.setState({ showModal: false });
render() { render() {
const { split, themePreferences, theme } = this.state; const { themePreferences, theme } = this.state;
let content = (
<App
ref={(navigatorRef) => {
Navigation.setTopLevelNavigator(navigatorRef);
}}
screenProps={{ split, theme }}
onNavigationStateChange={onNavigationStateChange}
/>
);
if (isTablet) {
const { inside, showModal } = this.state;
content = (
<SplitContext.Provider value={{ split }}>
<Tablet
theme={theme}
tablet={split}
inside={inside}
showModal={showModal}
closeModal={this.closeModal}
onLayout={this.onLayout}
>
{content}
</Tablet>
</SplitContext.Provider>
);
}
return ( return (
<AppearanceProvider> <AppearanceProvider>
<Provider store={store}> <Provider store={store}>
@ -741,7 +160,7 @@ export default class Root extends React.Component {
setTheme: this.setTheme setTheme: this.setTheme
}} }}
> >
{content} <AppContainer />
<TwoFactor /> <TwoFactor />
<ScreenLockedView /> <ScreenLockedView />
<ChangePasscodeView /> <ChangePasscodeView />

View File

@ -1,21 +0,0 @@
import { NavigationActions } from 'react-navigation';
let _navigatorModal;
function setTopLevelNavigator(navigatorRef) {
_navigatorModal = navigatorRef;
}
function navigate(routeName, params) {
_navigatorModal.dispatch(
NavigationActions.navigate({
routeName,
params
})
);
}
export default {
navigate,
setTopLevelNavigator
};

View File

@ -1,28 +1,25 @@
import { NavigationActions } from 'react-navigation'; import * as React from 'react';
import { CommonActions, StackActions } from '@react-navigation/native';
let _navigator; const navigationRef = React.createRef();
const routeNameRef = React.createRef();
function setTopLevelNavigator(navigatorRef) { function navigate(name, params) {
_navigator = navigatorRef; navigationRef.current?.navigate(name, params);
} }
function back() { function back() {
_navigator.dispatch( navigationRef.current?.dispatch(CommonActions.goBack());
NavigationActions.back()
);
} }
function navigate(routeName, params) { function replace(name, params) {
_navigator.dispatch( navigationRef.current?.dispatch(StackActions.replace(name, params));
NavigationActions.navigate({
routeName,
params
})
);
} }
export default { export default {
back, navigationRef,
routeNameRef,
navigate, navigate,
setTopLevelNavigator back,
replace
}; };

View File

@ -1,21 +1,9 @@
import { NavigationActions } from 'react-navigation'; import { createRef } from 'react';
let _shareNavigator; const navigationRef = createRef();
const routeNameRef = createRef();
function setTopLevelNavigator(navigatorRef) {
_shareNavigator = navigatorRef;
}
function navigate(routeName, params) {
_shareNavigator.dispatch(
NavigationActions.navigate({
routeName,
params
})
);
}
export default { export default {
navigate, navigationRef,
setTopLevelNavigator routeNameRef
}; };

View File

@ -6,8 +6,14 @@ async function load({ rid: roomId, latest, t }) {
if (latest) { if (latest) {
params = { ...params, latest: new Date(latest).toISOString() }; params = { ...params, latest: new Date(latest).toISOString() };
} }
const apiType = this.roomTypeToApiType(t);
if (!apiType) {
return [];
}
// RC 0.48.0 // RC 0.48.0
const data = await this.sdk.get(`${ this.roomTypeToApiType(t) }.history`, params); const data = await this.sdk.get(`${ apiType }.history`, params);
if (!data || data.status === 'error') { if (!data || data.status === 'error') {
return []; return [];
} }

View File

@ -17,6 +17,9 @@ import sharedStyles from '../../views/Styles';
import { ROW_HEIGHT } from '../../presentation/RoomItem'; import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { getActiveRoute } from '../../utils/navigation';
import Navigation from '../../lib/Navigation';
import { goRoom } from '../../utils/goRoom';
const AVATAR_SIZE = 48; const AVATAR_SIZE = 48;
const ANIMATION_DURATION = 300; const ANIMATION_DURATION = 300;
@ -71,7 +74,7 @@ const styles = StyleSheet.create({
class NotificationBadge extends React.Component { class NotificationBadge extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object, isMasterDetail: PropTypes.bool,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
notification: PropTypes.object, notification: PropTypes.object,
@ -102,16 +105,20 @@ class NotificationBadge extends React.Component {
return false; return false;
} }
componentDidUpdate() { componentDidUpdate(prevProps) {
const { notification: { payload }, navigation } = this.props; const { notification: { payload } } = this.props;
const navState = this.getNavState(navigation.state); const { notification: { payload: prevPayload } } = prevProps;
if (!equal(prevPayload, payload)) {
const state = Navigation.navigationRef.current.getRootState();
const route = getActiveRoute(state);
if (payload.rid) { if (payload.rid) {
if (navState && navState.routeName === 'RoomView' && navState.params && navState.params.rid === payload.rid) { if (route?.name === 'RoomView' && route.params?.rid === payload.rid) {
return; return;
} }
this.show(); this.show();
} }
} }
}
componentWillUnmount() { componentWillUnmount() {
this.clearTimeout(); this.clearTimeout();
@ -150,15 +157,8 @@ class NotificationBadge extends React.Component {
} }
} }
getNavState = (routes) => { goToRoom = () => {
if (!routes.routes) { const { notification, isMasterDetail, baseUrl } = this.props;
return routes;
}
return this.getNavState(routes.routes[routes.index]);
}
goToRoom = async() => {
const { notification, navigation, baseUrl } = this.props;
const { payload } = notification; const { payload } = notification;
const { rid, type, prid } = payload; const { rid, type, prid } = payload;
if (!rid) { if (!rid) {
@ -167,10 +167,13 @@ class NotificationBadge extends React.Component {
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title will be null, so we use payload from notification // if sub is not on local database, title will be null, so we use payload from notification
const { title = name } = notification; const { title = name } = notification;
await navigation.navigate('RoomsListView'); const item = {
navigation.navigate('RoomView', {
rid, name: title, t: type, prid, baseUrl rid, name: title, t: type, prid, baseUrl
}); };
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
}
goRoom({ item, isMasterDetail });
this.hide(); this.hide();
} }
@ -234,7 +237,8 @@ class NotificationBadge extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
user: getUserSelector(state), user: getUserSelector(state),
baseUrl: state.server.server, baseUrl: state.server.server,
notification: state.notification notification: state.notification,
isMasterDetail: PropTypes.bool
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View File

@ -25,7 +25,8 @@ class Touchable extends React.Component {
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
hideChannel: PropTypes.func, hideChannel: PropTypes.func,
children: PropTypes.element, children: PropTypes.element,
theme: PropTypes.string theme: PropTypes.string,
isFocused: PropTypes.bool
} }
constructor(props) { constructor(props) {
@ -167,7 +168,7 @@ class Touchable extends React.Component {
render() { render() {
const { const {
testID, isRead, width, favorite, children, theme testID, isRead, width, favorite, children, theme, isFocused
} = this.props; } = this.props;
return ( return (
@ -203,7 +204,7 @@ class Touchable extends React.Component {
theme={theme} theme={theme}
testID={testID} testID={testID}
style={{ style={{
backgroundColor: themes[theme].backgroundColor backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}} }}
> >
{children} {children}

View File

@ -28,7 +28,8 @@ const attrs = [
'favorite', 'favorite',
'status', 'status',
'connected', 'connected',
'theme' 'theme',
'isFocused'
]; ];
const arePropsEqual = (oldProps, newProps) => { const arePropsEqual = (oldProps, newProps) => {
@ -41,7 +42,39 @@ const arePropsEqual = (oldProps, newProps) => {
}; };
const RoomItem = React.memo(({ const RoomItem = React.memo(({
onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, isGroupChat, connected, theme onPress,
width,
favorite,
toggleFav,
isRead,
rid,
toggleRead,
hideChannel,
testID,
unread,
userMentions,
name,
_updatedAt,
alert,
type,
avatarSize,
baseUrl,
userId,
username,
token,
id,
prid,
showLastMessage,
hideUnreadStatus,
lastMessage,
status,
avatar,
useRealName,
getUserPresence,
isGroupChat,
connected,
theme,
isFocused
}) => { }) => {
useEffect(() => { useEffect(() => {
if (connected && type === 'd' && id) { if (connected && type === 'd' && id) {
@ -79,6 +112,7 @@ const RoomItem = React.memo(({
testID={testID} testID={testID}
type={type} type={type}
theme={theme} theme={theme}
isFocused={isFocused}
> >
<View <View
style={styles.container} style={styles.container}
@ -200,7 +234,8 @@ RoomItem.propTypes = {
getUserPresence: PropTypes.func, getUserPresence: PropTypes.func,
connected: PropTypes.bool, connected: PropTypes.bool,
isGroupChat: PropTypes.bool, isGroupChat: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string,
isFocused: PropTypes.bool
}; };
RoomItem.defaultProps = { RoomItem.defaultProps = {

View File

@ -2,6 +2,8 @@ import { APP, APP_STATE } from '../actions/actionsTypes';
const initialState = { const initialState = {
root: null, root: null,
isMasterDetail: false,
text: null,
ready: false, ready: false,
foreground: true, foreground: true,
background: false background: false
@ -24,7 +26,8 @@ export default function app(state = initialState, action) {
case APP.START: case APP.START:
return { return {
...state, ...state,
root: action.root root: action.root,
text: action.text
}; };
case APP.INIT: case APP.INIT:
return { return {
@ -36,6 +39,11 @@ export default function app(state = initialState, action) {
...state, ...state,
ready: true ready: true
}; };
case APP.SET_MASTER_DETAIL:
return {
...state,
isMasterDetail: action.isMasterDetail
};
default: default:
return state; return state;
} }

View File

@ -17,7 +17,7 @@ export default function(state = initialState, action) {
return { return {
...state, ...state,
rooms: state.rooms rooms: state.rooms
.filter(room => room.rid === action.rid) .filter(rid => rid !== action.rid)
}; };
case ROOM.LEAVE: case ROOM.LEAVE:
return { return {

View File

@ -7,7 +7,8 @@ const initialState = {
server: '', server: '',
version: null, version: null,
loading: true, loading: true,
adding: false adding: false,
previousServer: null
}; };
@ -54,12 +55,14 @@ export default function server(state = initialState, action) {
case SERVER.INIT_ADD: case SERVER.INIT_ADD:
return { return {
...state, ...state,
adding: true adding: true,
previousServer: action.previousServer
}; };
case SERVER.FINISH_ADD: case SERVER.FINISH_ADD:
return { return {
...state, ...state,
adding: false adding: false,
previousServer: null
}; };
default: default:
return state; return state;

View File

@ -10,6 +10,7 @@ import RocketChat from '../lib/rocketchat';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import database from '../lib/database'; import database from '../lib/database';
import I18n from '../i18n'; import I18n from '../i18n';
import { goRoom } from '../utils/goRoom';
const createChannel = function createChannel(data) { const createChannel = function createChannel(data) {
return RocketChat.createChannel(data); return RocketChat.createChannel(data);
@ -55,9 +56,12 @@ const handleRequest = function* handleRequest({ data }) {
} }
}; };
const handleSuccess = function handleSuccess({ data }) { const handleSuccess = function* handleSuccess({ data }) {
const { rid, t } = data; const isMasterDetail = yield select(state => state.app.isMasterDetail);
Navigation.navigate('RoomView', { rid, t, name: RocketChat.getRoomTitle(data) }); if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
}
goRoom({ item: data, isMasterDetail });
}; };
const handleFailure = function handleFailure({ err }) { const handleFailure = function handleFailure({ err }) {

View File

@ -10,8 +10,9 @@ import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks'
import database from '../lib/database'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { appStart } from '../actions'; import { appStart, ROOT_INSIDE } from '../actions/app';
import { localAuthenticate } from '../utils/localAuthentication'; import { localAuthenticate } from '../utils/localAuthentication';
import { goRoom } from '../utils/goRoom';
const roomTypes = { const roomTypes = {
channel: 'c', direct: 'd', group: 'p', channels: 'l' channel: 'c', direct: 'd', group: 'p', channels: 'l'
@ -29,19 +30,25 @@ const handleInviteLink = function* handleInviteLink({ params, requireLogin = fal
}; };
const navigate = function* navigate({ params }) { const navigate = function* navigate({ params }) {
yield put(appStart('inside')); yield put(appStart({ root: ROOT_INSIDE }));
if (params.path) { if (params.path) {
const [type, name] = params.path.split('/'); const [type, name] = params.path.split('/');
if (type !== 'invite') { if (type !== 'invite') {
const room = yield RocketChat.canOpenRoom(params); const room = yield RocketChat.canOpenRoom(params);
if (room) { if (room) {
yield Navigation.navigate('RoomsListView'); const isMasterDetail = yield select(state => state.app.isMasterDetail);
Navigation.navigate('RoomView', { if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
const item = {
name, name,
t: roomTypes[type], t: roomTypes[type],
roomUserId: RocketChat.getUidDirectMessage(room), roomUserId: RocketChat.getUidDirectMessage(room),
...room ...room
}); };
goRoom({ item, isMasterDetail });
} }
} else { } else {
yield handleInviteLink({ params }); yield handleInviteLink({ params });

View File

@ -4,14 +4,12 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNBootSplash from 'react-native-bootsplash'; import RNBootSplash from 'react-native-bootsplash';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import * as actions from '../actions';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import { setAllPreferences } from '../actions/sortPreferences'; import { setAllPreferences } from '../actions/sortPreferences';
import { toggleCrashReport } from '../actions/crashReport'; import { toggleCrashReport } from '../actions/crashReport';
import { APP } from '../actions/actionsTypes'; import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import Navigation from '../lib/Navigation';
import { import {
SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID
} from '../constants/userDefaults'; } from '../constants/userDefaults';
@ -19,6 +17,7 @@ import { isIOS } from '../utils/deviceInfo';
import database from '../lib/database'; import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction'; import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { localAuthenticate } from '../utils/localAuthentication'; import { localAuthenticate } from '../utils/localAuthentication';
import { appStart, ROOT_OUTSIDE, appReady } from '../actions/app';
export const initLocalSettings = function* initLocalSettings() { export const initLocalSettings = function* initLocalSettings() {
const sortPreferences = yield RocketChat.getSortPreferences(); const sortPreferences = yield RocketChat.getSortPreferences();
@ -96,7 +95,7 @@ const restore = function* restore() {
RNUserDefaults.clear(RocketChat.TOKEN_KEY), RNUserDefaults.clear(RocketChat.TOKEN_KEY),
RNUserDefaults.clear('currentServer') RNUserDefaults.clear('currentServer')
]); ]);
yield put(actions.appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
} else { } else {
const serversDB = database.servers; const serversDB = database.servers;
const serverCollections = serversDB.collections.get('servers'); const serverCollections = serversDB.collections.get('servers');
@ -106,23 +105,14 @@ const restore = function* restore() {
yield put(selectServerRequest(server, serverObj && serverObj.version)); yield put(selectServerRequest(server, serverObj && serverObj.version));
} }
yield put(actions.appReady({})); yield put(appReady({}));
} catch (e) { } catch (e) {
log(e); log(e);
yield put(actions.appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
} }
}; };
const start = function* start({ root, text }) { const start = function start() {
if (root === 'inside') {
yield Navigation.navigate('InsideStack');
} else if (root === 'setUsername') {
yield Navigation.navigate('SetUsernameStack');
} else if (root === 'outside') {
yield Navigation.navigate('OutsideStack');
} else if (root === 'loading') {
yield Navigation.navigate('AuthLoading', { text });
}
RNBootSplash.hide(); RNBootSplash.hide();
}; };

View File

@ -7,7 +7,9 @@ import moment from 'moment';
import 'moment/min/locales'; import 'moment/min/locales';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { appStart } from '../actions'; import {
appStart, ROOT_SET_USERNAME, ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE
} from '../actions/app';
import { serverFinishAdd, selectServerRequest } from '../actions/server'; import { serverFinishAdd, selectServerRequest } from '../actions/server';
import { import {
loginFailure, loginSuccess, setUser, logout loginFailure, loginSuccess, setUser, logout
@ -40,7 +42,7 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE
if (!result.username) { if (!result.username) {
yield put(serverFinishAdd()); yield put(serverFinishAdd());
yield put(setUser(result)); yield put(setUser(result));
yield put(appStart('setUsername')); yield put(appStart({ root: ROOT_SET_USERNAME }));
} else { } else {
const server = yield select(getServer); const server = yield select(getServer);
yield localAuthenticate(server); yield localAuthenticate(server);
@ -133,17 +135,17 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
let currentRoot; let currentRoot;
if (adding) { if (adding) {
yield put(serverFinishAdd()); yield put(serverFinishAdd());
yield put(appStart('inside')); yield put(appStart({ root: ROOT_INSIDE }));
} else { } else {
currentRoot = yield select(state => state.app.root); currentRoot = yield select(state => state.app.root);
if (currentRoot !== 'inside') { if (currentRoot !== ROOT_INSIDE) {
yield put(appStart('inside')); yield put(appStart({ root: ROOT_INSIDE }));
} }
} }
// after a successful login, check if it's been invited via invite link // after a successful login, check if it's been invited via invite link
currentRoot = yield select(state => state.app.root); currentRoot = yield select(state => state.app.root);
if (currentRoot === 'inside') { if (currentRoot === ROOT_INSIDE) {
const inviteLinkToken = yield select(state => state.inviteLinks.token); const inviteLinkToken = yield select(state => state.inviteLinks.token);
if (inviteLinkToken) { if (inviteLinkToken) {
yield put(inviteLinksRequest(inviteLinkToken)); yield put(inviteLinksRequest(inviteLinkToken));
@ -155,7 +157,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
}; };
const handleLogout = function* handleLogout({ forcedByServer }) { const handleLogout = function* handleLogout({ forcedByServer }) {
yield put(appStart('loading', I18n.t('Logging_out'))); yield put(appStart({ root: ROOT_LOADING, text: I18n.t('Logging_out') }));
const server = yield select(getServer); const server = yield select(getServer);
if (server) { if (server) {
try { try {
@ -163,7 +165,7 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
// if the user was logged out by the server // if the user was logged out by the server
if (forcedByServer) { if (forcedByServer) {
yield put(appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops')); showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops'));
EventEmitter.emit('NewServer', { server }); EventEmitter.emit('NewServer', { server });
} else { } else {
@ -183,10 +185,10 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
} }
} }
// if there's no servers, go outside // if there's no servers, go outside
yield put(appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
} }
} catch (e) { } catch (e) {
yield put(appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
log(e); log(e);
} }
} }

View File

@ -1,4 +1,4 @@
import { takeLatest } from 'redux-saga/effects'; import { takeLatest, select } from 'redux-saga/effects';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
@ -6,32 +6,28 @@ import { MESSAGES } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/database'; import database from '../lib/database';
import log from '../utils/log'; import log from '../utils/log';
import { goRoom } from '../utils/goRoom';
const goRoom = function goRoom({
rid, name, fname, message
}) {
Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', {
rid, name, fname, t: 'd', message
});
};
const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
try { try {
const db = database.active; const db = database.active;
const { username, name } = message.u; const { username } = message.u;
const subsCollection = db.collections.get('subscriptions'); const subsCollection = db.collections.get('subscriptions');
const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch(); const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch();
const isMasterDetail = yield select(state => state.app.isMasterDetail);
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
if (subscriptions.length) { if (subscriptions.length) {
yield goRoom({ goRoom({ item: subscriptions[0], isMasterDetail, message });
rid: subscriptions[0].rid, name: username, fname: name, message
});
} else { } else {
const result = yield RocketChat.createDirectMessage(username); const result = yield RocketChat.createDirectMessage(username);
if (result?.success) { if (result?.success) {
yield goRoom({ goRoom({ item: result?.room, isMasterDetail, message });
rid: result?.room.rid, t: 'd', name: username, fname: name, message
});
} }
} }
} catch (e) { } catch (e) {

View File

@ -31,7 +31,12 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
}; };
const handleRemovedRoom = function* handleRemovedRoom() { const handleRemovedRoom = function* handleRemovedRoom() {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
if (isMasterDetail) {
yield Navigation.navigate('DrawerNavigator');
} else {
yield Navigation.navigate('RoomsListView'); yield Navigation.navigate('RoomsListView');
}
// types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg // types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg
const { timeout } = yield race({ const { timeout } = yield race({
deleteFinished: take(types.ROOM.REMOVED), deleteFinished: take(types.ROOM.REMOVED),
@ -74,7 +79,12 @@ const handleCloseRoom = function* handleCloseRoom({ rid }) {
const closeRoom = async(comment = '') => { const closeRoom = async(comment = '') => {
try { try {
await RocketChat.closeLivechat(rid, comment); await RocketChat.closeLivechat(rid, comment);
const isMasterDetail = await select(state => state.app.isMasterDetail);
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
}
} catch { } catch {
// do nothing // do nothing
} }
@ -105,7 +115,12 @@ const handleForwardRoom = function* handleForwardRoom({ transferData }) {
try { try {
const result = yield RocketChat.forwardLivechat(transferData); const result = yield RocketChat.forwardLivechat(transferData);
if (result === true) { if (result === true) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
}
} else { } else {
showErrorAlert(I18n.t('No_available_agents_to_transfer'), I18n.t('Oops')); showErrorAlert(I18n.t('No_available_agents_to_transfer'), I18n.t('Oops'));
} }

View File

@ -6,7 +6,6 @@ import semver from 'semver';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { SERVER } from '../actions/actionsTypes'; import { SERVER } from '../actions/actionsTypes';
import * as actions from '../actions';
import { import {
serverFailure, selectServerRequest, selectServerSuccess, selectServerFailure serverFailure, selectServerRequest, selectServerSuccess, selectServerFailure
} from '../actions/server'; } from '../actions/server';
@ -19,6 +18,7 @@ import { extractHostname } from '../utils/server';
import I18n from '../i18n'; import I18n from '../i18n';
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch'; import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
const getServerInfo = function* getServerInfo({ server, raiseError = true }) { const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try { try {
@ -103,10 +103,10 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
yield put(clearSettings()); yield put(clearSettings());
yield RocketChat.connect({ server, user, logoutOnError: true }); yield RocketChat.connect({ server, user, logoutOnError: true });
yield put(setUser(user)); yield put(setUser(user));
yield put(actions.appStart('inside')); yield put(appStart({ root: ROOT_INSIDE }));
} else { } else {
yield RocketChat.connect({ server }); yield RocketChat.connect({ server });
yield put(actions.appStart('outside')); yield put(appStart({ root: ROOT_OUTSIDE }));
} }
// We can't use yield here because fetch of Settings & Custom Emojis is slower // We can't use yield here because fetch of Settings & Custom Emojis is slower

View File

@ -5,10 +5,11 @@ import { setBadgeCount } from '../notifications/push';
import log from '../utils/log'; import log from '../utils/log';
import { localAuthenticate, saveLastLocalAuthenticationSession } from '../utils/localAuthentication'; import { localAuthenticate, saveLastLocalAuthenticationSession } from '../utils/localAuthentication';
import { APP_STATE } from '../actions/actionsTypes'; import { APP_STATE } from '../actions/actionsTypes';
import { ROOT_OUTSIDE } from '../actions/app';
const appHasComeBackToForeground = function* appHasComeBackToForeground() { const appHasComeBackToForeground = function* appHasComeBackToForeground() {
const appRoot = yield select(state => state.app.root); const appRoot = yield select(state => state.app.root);
if (appRoot === 'outside') { if (appRoot === ROOT_OUTSIDE) {
return; return;
} }
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
@ -27,7 +28,7 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() {
const appHasComeBackToBackground = function* appHasComeBackToBackground() { const appHasComeBackToBackground = function* appHasComeBackToBackground() {
const appRoot = yield select(state => state.app.root); const appRoot = yield select(state => state.app.root);
if (appRoot === 'outside') { if (appRoot === ROOT_OUTSIDE) {
return; return;
} }
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { useContext } from 'react';
import { View } from 'react-native'; import PropTypes from 'prop-types';
import { createAppContainer, createSwitchNavigator } from 'react-navigation'; import { NavigationContainer } from '@react-navigation/native';
import { AppearanceProvider } from 'react-native-appearance'; import { AppearanceProvider } from 'react-native-appearance';
import { createStackNavigator } from 'react-navigation-stack'; import { createStackNavigator } from '@react-navigation/stack';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
@ -14,61 +14,114 @@ import {
} from './utils/theme'; } from './utils/theme';
import Navigation from './lib/ShareNavigation'; import Navigation from './lib/ShareNavigation';
import store from './lib/createStore'; import store from './lib/createStore';
import sharedStyles from './views/Styles'; import { supportSystemTheme } from './utils/deviceInfo';
import { hasNotch, supportSystemTheme } from './utils/deviceInfo'; import {
import { defaultHeader, onNavigationStateChange, cardStyle } from './utils/navigation'; defaultHeader, themedHeader, getActiveRouteName, navigationTheme
} from './utils/navigation';
import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat';
import { ThemeContext } from './theme'; import { ThemeContext } from './theme';
import { localAuthenticate } from './utils/localAuthentication'; import { localAuthenticate } from './utils/localAuthentication';
import ScreenLockedView from './views/ScreenLockedView'; import ScreenLockedView from './views/ScreenLockedView';
const InsideNavigator = createStackNavigator({ // Outside Stack
ShareListView: { import WithoutServersView from './views/WithoutServersView';
getScreen: () => require('./views/ShareListView').default
},
ShareView: {
getScreen: () => require('./views/ShareView').default
},
SelectServerView: {
getScreen: () => require('./views/SelectServerView').default
}
}, {
initialRouteName: 'ShareListView',
defaultNavigationOptions: defaultHeader,
cardStyle
});
const OutsideNavigator = createStackNavigator({ // Inside Stack
WithoutServersView: { import ShareListView from './views/ShareListView';
getScreen: () => require('./views/WithoutServersView').default import ShareView from './views/ShareView';
} import SelectServerView from './views/SelectServerView';
}, { import { setCurrentScreen } from './utils/log';
initialRouteName: 'WithoutServersView', import AuthLoadingView from './views/AuthLoadingView';
defaultNavigationOptions: defaultHeader,
cardStyle
});
const AppContainer = createAppContainer(createSwitchNavigator({ const Inside = createStackNavigator();
OutsideStack: OutsideNavigator, const InsideStack = () => {
InsideStack: InsideNavigator, const { theme } = useContext(ThemeContext);
AuthLoading: {
getScreen: () => require('./views/AuthLoadingView').default const screenOptions = {
} ...defaultHeader,
}, ...themedHeader(theme)
{ };
initialRouteName: 'AuthLoading' screenOptions.headerStyle = {
})); ...screenOptions.headerStyle,
// TODO: fix on multiple files PR :)
height: 57
};
return (
<Inside.Navigator screenOptions={screenOptions}>
<Inside.Screen
name='ShareListView'
component={ShareListView}
/>
<Inside.Screen
name='ShareView'
component={ShareView}
/>
<Inside.Screen
name='SelectServerView'
component={SelectServerView}
options={SelectServerView.navigationOptions}
/>
</Inside.Navigator>
);
};
const Outside = createStackNavigator();
const OutsideStack = () => {
const { theme } = useContext(ThemeContext);
return (
<Outside.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme) }}>
<Outside.Screen
name='WithoutServersView'
component={WithoutServersView}
options={WithoutServersView.navigationOptions}
/>
</Outside.Navigator>
);
};
// App
const Stack = createStackNavigator();
export const App = ({ root }) => (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<>
{!root ? (
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === 'outside' ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === 'inside' ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
</>
</Stack.Navigator>
);
App.propTypes = {
root: PropTypes.string
};
class Root extends React.Component { class Root extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isLandscape: false,
theme: defaultTheme(), theme: defaultTheme(),
themePreferences: { themePreferences: {
currentTheme: supportSystemTheme() ? 'automatic' : 'light', currentTheme: supportSystemTheme() ? 'automatic' : 'light',
darkLevel: 'dark' darkLevel: 'dark'
} },
root: ''
}; };
this.init(); this.init();
} }
@ -85,11 +138,16 @@ class Root extends React.Component {
if (currentServer && token) { if (currentServer && token) {
await localAuthenticate(currentServer); await localAuthenticate(currentServer);
await Navigation.navigate('InsideStack'); this.setState({ root: 'inside' });
await RocketChat.shareExtensionInit(currentServer); await RocketChat.shareExtensionInit(currentServer);
} else { } else {
await Navigation.navigate('OutsideStack'); this.setState({ root: 'outside' });
} }
const state = Navigation.navigationRef.current.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
} }
setTheme = (newTheme = {}) => { setTheme = (newTheme = {}) => {
@ -101,32 +159,30 @@ class Root extends React.Component {
}); });
} }
handleLayout = (event) => {
const { width, height } = event.nativeEvent.layout;
this.setState({ isLandscape: width > height });
}
render() { render() {
const { isLandscape, theme } = this.state; const { theme, root } = this.state;
const navTheme = navigationTheme(theme);
return ( return (
<AppearanceProvider> <AppearanceProvider>
<View
style={[sharedStyles.container, isLandscape && hasNotch ? sharedStyles.notchLandscapeContainer : {}]}
onLayout={this.handleLayout}
>
<Provider store={store}> <Provider store={store}>
<ThemeContext.Provider value={{ theme }}> <ThemeContext.Provider value={{ theme }}>
<AppContainer <NavigationContainer
ref={(navigatorRef) => { theme={navTheme}
Navigation.setTopLevelNavigator(navigatorRef); ref={Navigation.navigationRef}
onStateChange={(state) => {
const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName);
}
Navigation.routeNameRef.current = currentRouteName;
}} }}
onNavigationStateChange={onNavigationStateChange} >
screenProps={{ theme }} <App root={root} />
/> </NavigationContainer>
<ScreenLockedView /> <ScreenLockedView />
</ThemeContext.Provider> </ThemeContext.Provider>
</Provider> </Provider>
</View>
</AppearanceProvider> </AppearanceProvider>
); );
} }

View File

@ -1,19 +0,0 @@
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { isTablet } from './utils/deviceInfo';
export const SplitContext = React.createContext(null);
export function withSplit(Component) {
if (isTablet) {
const SplitComponent = props => (
<SplitContext.Consumer>
{contexts => <Component {...props} {...contexts} />}
</SplitContext.Consumer>
);
hoistNonReactStatics(SplitComponent, Component);
return SplitComponent;
}
return Component;
}

335
app/stacks/InsideStack.js Normal file
View File

@ -0,0 +1,335 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createStackNavigator } from '@react-navigation/stack';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { ThemeContext } from '../theme';
import {
defaultHeader, themedHeader, ModalAnimation, StackAnimation
} from '../utils/navigation';
import Toast from '../containers/Toast';
import Sidebar from '../views/SidebarView';
import NotificationBadge from '../notifications/inApp';
// Chats Stack
import RoomView from '../views/RoomView';
import RoomsListView from '../views/RoomsListView';
import RoomActionsView from '../views/RoomActionsView';
import RoomInfoView from '../views/RoomInfoView';
import RoomInfoEditView from '../views/RoomInfoEditView';
import RoomMembersView from '../views/RoomMembersView';
import SearchMessagesView from '../views/SearchMessagesView';
import SelectedUsersView from '../views/SelectedUsersView';
import InviteUsersView from '../views/InviteUsersView';
import InviteUsersEditView from '../views/InviteUsersEditView';
import MessagesView from '../views/MessagesView';
import AutoTranslateView from '../views/AutoTranslateView';
import DirectoryView from '../views/DirectoryView';
import NotificationPrefView from '../views/NotificationPreferencesView';
import VisitorNavigationView from '../views/VisitorNavigationView';
import ForwardLivechatView from '../views/ForwardLivechatView';
import LivechatEditView from '../views/LivechatEditView';
import PickerView from '../views/PickerView';
import ThreadMessagesView from '../views/ThreadMessagesView';
import MarkdownTableView from '../views/MarkdownTableView';
import ReadReceiptsView from '../views/ReadReceiptView';
// Profile Stack
import ProfileView from '../views/ProfileView';
// Settings Stack
import SettingsView from '../views/SettingsView';
import LanguageView from '../views/LanguageView';
import ThemeView from '../views/ThemeView';
import DefaultBrowserView from '../views/DefaultBrowserView';
import ScreenLockConfigView from '../views/ScreenLockConfigView';
// Admin Stack
import AdminPanelView from '../views/AdminPanelView';
// NewMessage Stack
import NewMessageView from '../views/NewMessageView';
import CreateChannelView from '../views/CreateChannelView';
// InsideStackNavigator
import AttachmentView from '../views/AttachmentView';
import ModalBlockView from '../views/ModalBlockView';
import JitsiMeetView from '../views/JitsiMeetView';
import StatusView from '../views/StatusView';
import CreateDiscussionView from '../views/CreateDiscussionView';
// ChatsStackNavigator
const ChatsStack = createStackNavigator();
const ChatsStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<ChatsStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<ChatsStack.Screen
name='RoomsListView'
component={RoomsListView}
/>
<ChatsStack.Screen
name='RoomView'
component={RoomView}
/>
<ChatsStack.Screen
name='RoomActionsView'
component={RoomActionsView}
options={RoomActionsView.navigationOptions}
/>
<ChatsStack.Screen
name='RoomInfoView'
component={RoomInfoView}
options={RoomInfoView.navigationOptions}
/>
<ChatsStack.Screen
name='RoomInfoEditView'
component={RoomInfoEditView}
options={RoomInfoEditView.navigationOptions}
/>
<ChatsStack.Screen
name='RoomMembersView'
component={RoomMembersView}
options={RoomMembersView.navigationOptions}
/>
<ChatsStack.Screen
name='SearchMessagesView'
component={SearchMessagesView}
options={SearchMessagesView.navigationOptions}
/>
<ChatsStack.Screen
name='SelectedUsersView'
component={SelectedUsersView}
/>
<ChatsStack.Screen
name='InviteUsersView'
component={InviteUsersView}
options={InviteUsersView.navigationOptions}
/>
<ChatsStack.Screen
name='InviteUsersEditView'
component={InviteUsersEditView}
options={InviteUsersEditView.navigationOptions}
/>
<ChatsStack.Screen
name='MessagesView'
component={MessagesView}
options={MessagesView.navigationOptions}
/>
<ChatsStack.Screen
name='AutoTranslateView'
component={AutoTranslateView}
options={AutoTranslateView.navigationOptions}
/>
<ChatsStack.Screen
name='DirectoryView'
component={DirectoryView}
options={DirectoryView.navigationOptions}
/>
<ChatsStack.Screen
name='NotificationPrefView'
component={NotificationPrefView}
options={NotificationPrefView.navigationOptions}
/>
<ChatsStack.Screen
name='VisitorNavigationView'
component={VisitorNavigationView}
options={VisitorNavigationView.navigationOptions}
/>
<ChatsStack.Screen
name='ForwardLivechatView'
component={ForwardLivechatView}
options={ForwardLivechatView.navigationOptions}
/>
<ChatsStack.Screen
name='LivechatEditView'
component={LivechatEditView}
options={LivechatEditView.navigationOptions}
/>
<ChatsStack.Screen
name='PickerView'
component={PickerView}
options={PickerView.navigationOptions}
/>
<ChatsStack.Screen
name='ThreadMessagesView'
component={ThreadMessagesView}
options={ThreadMessagesView.navigationOptions}
/>
<ChatsStack.Screen
name='MarkdownTableView'
component={MarkdownTableView}
options={MarkdownTableView.navigationOptions}
/>
<ChatsStack.Screen
name='ReadReceiptsView'
component={ReadReceiptsView}
options={ReadReceiptsView.navigationOptions}
/>
</ChatsStack.Navigator>
);
};
// ProfileStackNavigator
const ProfileStack = createStackNavigator();
const ProfileStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<ProfileStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<ProfileStack.Screen
name='ProfileView'
component={ProfileView}
options={ProfileView.navigationOptions}
/>
</ProfileStack.Navigator>
);
};
// SettingsStackNavigator
const SettingsStack = createStackNavigator();
const SettingsStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<SettingsStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<SettingsStack.Screen
name='SettingsView'
component={SettingsView}
options={SettingsView.navigationOptions}
/>
<SettingsStack.Screen
name='LanguageView'
component={LanguageView}
options={LanguageView.navigationOptions}
/>
<SettingsStack.Screen
name='ThemeView'
component={ThemeView}
options={ThemeView.navigationOptions}
/>
<SettingsStack.Screen
name='DefaultBrowserView'
component={DefaultBrowserView}
options={DefaultBrowserView.navigationOptions}
/>
<SettingsStack.Screen
name='ScreenLockConfigView'
component={ScreenLockConfigView}
options={ScreenLockConfigView.navigationOptions}
/>
</SettingsStack.Navigator>
);
};
// AdminPanelStackNavigator
const AdminPanelStack = createStackNavigator();
const AdminPanelStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<AdminPanelStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<AdminPanelStack.Screen
name='AdminPanelView'
component={AdminPanelView}
options={AdminPanelView.navigationOptions}
/>
</AdminPanelStack.Navigator>
);
};
// DrawerNavigator
const Drawer = createDrawerNavigator();
const DrawerNavigator = () => (
<Drawer.Navigator
drawerContent={({ navigation, state }) => <Sidebar navigation={navigation} state={state} />}
screenOptions={{ swipeEnabled: false }}
drawerType='back'
>
<Drawer.Screen name='ChatsStackNavigator' component={ChatsStackNavigator} />
<Drawer.Screen name='ProfileStackNavigator' component={ProfileStackNavigator} />
<Drawer.Screen name='SettingsStackNavigator' component={SettingsStackNavigator} />
<Drawer.Screen name='AdminPanelStackNavigator' component={AdminPanelStackNavigator} />
</Drawer.Navigator>
);
// NewMessageStackNavigator
const NewMessageStack = createStackNavigator();
const NewMessageStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<NewMessageStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<NewMessageStack.Screen
name='NewMessageView'
component={NewMessageView}
options={NewMessageView.navigationOptions}
/>
<NewMessageStack.Screen
name='SelectedUsersViewCreateChannel'
component={SelectedUsersView}
/>
<NewMessageStack.Screen
name='CreateChannelView'
component={CreateChannelView}
options={CreateChannelView.navigationOptions}
/>
<NewMessageStack.Screen
name='CreateDiscussionView'
component={CreateDiscussionView}
/>
</NewMessageStack.Navigator>
);
};
// InsideStackNavigator
const InsideStack = createStackNavigator();
const InsideStackNavigator = () => {
const { theme } = React.useContext(ThemeContext);
return (
<InsideStack.Navigator mode='modal' screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...ModalAnimation }}>
<InsideStack.Screen
name='DrawerNavigator'
component={DrawerNavigator}
options={{ headerShown: false }}
/>
<InsideStack.Screen
name='NewMessageStackNavigator'
component={NewMessageStackNavigator}
options={{ headerShown: false }}
/>
<InsideStack.Screen
name='AttachmentView'
component={AttachmentView}
/>
<InsideStack.Screen
name='StatusView'
component={StatusView}
/>
<InsideStack.Screen
name='ModalBlockView'
component={ModalBlockView}
options={ModalBlockView.navigationOptions}
/>
<InsideStack.Screen
name='JitsiMeetView'
component={JitsiMeetView}
options={{ headerShown: false }}
/>
</InsideStack.Navigator>
);
};
const RootInsideStack = ({ navigation, route }) => (
<>
<InsideStackNavigator navigation={navigation} />
<NotificationBadge navigation={navigation} route={route} />
<Toast />
</>
);
RootInsideStack.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default RootInsideStack;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { TouchableWithoutFeedback, View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
root: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
backdrop: {
...StyleSheet.absoluteFill
}
});
export const ModalContainer = ({ navigation, children, theme }) => (
<View style={[styles.root, { backgroundColor: `${ themes[theme].backdropColor }70` }]}>
<TouchableWithoutFeedback onPress={() => navigation.pop()}>
<View style={styles.backdrop} />
</TouchableWithoutFeedback>
<View style={sharedStyles.modalFormSheet}>
{children}
</View>
</View>
);
ModalContainer.propTypes = {
navigation: PropTypes.object,
children: PropTypes.element,
theme: PropTypes.string
};

View File

@ -0,0 +1,307 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useIsFocused } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { ThemeContext } from '../../theme';
import {
defaultHeader, themedHeader, StackAnimation, FadeFromCenterModal
} from '../../utils/navigation';
import Toast from '../../containers/Toast';
import NotificationBadge from '../../notifications/inApp';
import { ModalContainer } from './ModalContainer';
// Chats Stack
import RoomView from '../../views/RoomView';
import RoomsListView from '../../views/RoomsListView';
import RoomActionsView from '../../views/RoomActionsView';
import RoomInfoView from '../../views/RoomInfoView';
import RoomInfoEditView from '../../views/RoomInfoEditView';
import RoomMembersView from '../../views/RoomMembersView';
import SearchMessagesView from '../../views/SearchMessagesView';
import SelectedUsersView from '../../views/SelectedUsersView';
import InviteUsersView from '../../views/InviteUsersView';
import InviteUsersEditView from '../../views/InviteUsersEditView';
import MessagesView from '../../views/MessagesView';
import AutoTranslateView from '../../views/AutoTranslateView';
import DirectoryView from '../../views/DirectoryView';
import NotificationPrefView from '../../views/NotificationPreferencesView';
import VisitorNavigationView from '../../views/VisitorNavigationView';
import ForwardLivechatView from '../../views/ForwardLivechatView';
import LivechatEditView from '../../views/LivechatEditView';
import PickerView from '../../views/PickerView';
import ThreadMessagesView from '../../views/ThreadMessagesView';
import MarkdownTableView from '../../views/MarkdownTableView';
import ReadReceiptsView from '../../views/ReadReceiptView';
import ProfileView from '../../views/ProfileView';
import SettingsView from '../../views/SettingsView';
import LanguageView from '../../views/LanguageView';
import ThemeView from '../../views/ThemeView';
import DefaultBrowserView from '../../views/DefaultBrowserView';
import ScreenLockConfigView from '../../views/ScreenLockConfigView';
import AdminPanelView from '../../views/AdminPanelView';
import NewMessageView from '../../views/NewMessageView';
import CreateChannelView from '../../views/CreateChannelView';
// InsideStackNavigator
import AttachmentView from '../../views/AttachmentView';
import ModalBlockView from '../../views/ModalBlockView';
import JitsiMeetView from '../../views/JitsiMeetView';
import StatusView from '../../views/StatusView';
import CreateDiscussionView from '../../views/CreateDiscussionView';
import { setKeyCommands, deleteKeyCommands } from '../../commands';
// ChatsStackNavigator
const ChatsStack = createStackNavigator();
const ChatsStackNavigator = React.memo(() => {
const { theme } = React.useContext(ThemeContext);
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
setKeyCommands();
} else {
deleteKeyCommands();
}
return () => {
deleteKeyCommands();
};
}, [isFocused]);
return (
<ChatsStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<ChatsStack.Screen
name='RoomView'
component={RoomView}
options={{ headerShown: false }}
/>
</ChatsStack.Navigator>
);
});
// DrawerNavigator
const Drawer = createDrawerNavigator();
const DrawerNavigator = React.memo(() => (
<Drawer.Navigator
drawerContent={({ navigation, state }) => <RoomsListView navigation={navigation} state={state} />}
drawerType='permanent'
>
<Drawer.Screen name='ChatsStackNavigator' component={ChatsStackNavigator} />
</Drawer.Navigator>
));
const ModalStack = createStackNavigator();
const ModalStackNavigator = React.memo(({ navigation }) => {
const { theme } = React.useContext(ThemeContext);
return (
<ModalContainer navigation={navigation} theme={theme}>
<ModalStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
<ModalStack.Screen
name='RoomActionsView'
component={RoomActionsView}
options={props => RoomActionsView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='RoomInfoView'
component={RoomInfoView}
options={RoomInfoView.navigationOptions}
/>
<ModalStack.Screen
name='RoomInfoEditView'
component={RoomInfoEditView}
options={RoomInfoEditView.navigationOptions}
/>
<ModalStack.Screen
name='RoomMembersView'
component={RoomMembersView}
options={RoomMembersView.navigationOptions}
/>
<ModalStack.Screen
name='SearchMessagesView'
component={SearchMessagesView}
options={SearchMessagesView.navigationOptions}
/>
<ModalStack.Screen
name='SelectedUsersView'
component={SelectedUsersView}
/>
<ModalStack.Screen
name='InviteUsersView'
component={InviteUsersView}
options={InviteUsersView.navigationOptions}
/>
<ModalStack.Screen
name='InviteUsersEditView'
component={InviteUsersEditView}
options={InviteUsersEditView.navigationOptions}
/>
<ModalStack.Screen
name='MessagesView'
component={MessagesView}
options={MessagesView.navigationOptions}
/>
<ModalStack.Screen
name='AutoTranslateView'
component={AutoTranslateView}
options={AutoTranslateView.navigationOptions}
/>
<ModalStack.Screen
name='DirectoryView'
component={DirectoryView}
options={props => DirectoryView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='NotificationPrefView'
component={NotificationPrefView}
options={NotificationPrefView.navigationOptions}
/>
<ModalStack.Screen
name='VisitorNavigationView'
component={VisitorNavigationView}
options={VisitorNavigationView.navigationOptions}
/>
<ModalStack.Screen
name='ForwardLivechatView'
component={ForwardLivechatView}
options={ForwardLivechatView.navigationOptions}
/>
<ModalStack.Screen
name='LivechatEditView'
component={LivechatEditView}
options={LivechatEditView.navigationOptions}
/>
<ModalStack.Screen
name='PickerView'
component={PickerView}
options={PickerView.navigationOptions}
/>
<ModalStack.Screen
name='ThreadMessagesView'
component={ThreadMessagesView}
options={props => ThreadMessagesView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='MarkdownTableView'
component={MarkdownTableView}
options={MarkdownTableView.navigationOptions}
/>
<ModalStack.Screen
name='ReadReceiptsView'
component={ReadReceiptsView}
options={ReadReceiptsView.navigationOptions}
/>
<ModalStack.Screen
name='SettingsView'
component={SettingsView}
options={props => SettingsView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='LanguageView'
component={LanguageView}
options={LanguageView.navigationOptions}
/>
<ModalStack.Screen
name='ThemeView'
component={ThemeView}
options={ThemeView.navigationOptions}
/>
<ModalStack.Screen
name='DefaultBrowserView'
component={DefaultBrowserView}
options={DefaultBrowserView.navigationOptions}
/>
<ModalStack.Screen
name='ScreenLockConfigView'
component={ScreenLockConfigView}
options={ScreenLockConfigView.navigationOptions}
/>
<ModalStack.Screen
name='StatusView'
component={StatusView}
/>
<ModalStack.Screen
name='ProfileView'
component={ProfileView}
options={props => ProfileView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='AdminPanelView'
component={AdminPanelView}
options={props => AdminPanelView.navigationOptions({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen
name='NewMessageView'
component={NewMessageView}
options={NewMessageView.navigationOptions}
/>
<ModalStack.Screen
name='SelectedUsersViewCreateChannel'
component={SelectedUsersView}
/>
<ModalStack.Screen
name='CreateChannelView'
component={CreateChannelView}
options={CreateChannelView.navigationOptions}
/>
<ModalStack.Screen
name='CreateDiscussionView'
component={CreateDiscussionView}
/>
</ModalStack.Navigator>
</ModalContainer>
);
});
ModalStackNavigator.propTypes = {
navigation: PropTypes.object
};
// InsideStackNavigator
const InsideStack = createStackNavigator();
const InsideStackNavigator = React.memo(() => {
const { theme } = React.useContext(ThemeContext);
return (
<InsideStack.Navigator mode='modal' screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...FadeFromCenterModal }}>
<InsideStack.Screen
name='DrawerNavigator'
component={DrawerNavigator}
options={{ headerShown: false }}
/>
<InsideStack.Screen
name='ModalStackNavigator'
component={ModalStackNavigator}
options={{ headerShown: false }}
/>
<InsideStack.Screen
name='AttachmentView'
component={AttachmentView}
/>
<InsideStack.Screen
name='ModalBlockView'
component={ModalBlockView}
options={ModalBlockView.navigationOptions}
/>
<InsideStack.Screen
name='JitsiMeetView'
component={JitsiMeetView}
options={{ headerShown: false }}
/>
</InsideStack.Navigator>
);
});
const RootInsideStack = React.memo(({ navigation, route }) => (
<>
<InsideStackNavigator navigation={navigation} />
<NotificationBadge navigation={navigation} route={route} />
<Toast />
</>
));
RootInsideStack.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default RootInsideStack;

101
app/stacks/OutsideStack.js Normal file
View File

@ -0,0 +1,101 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ThemeContext } from '../theme';
import {
defaultHeader, themedHeader, StackAnimation, ModalAnimation
} from '../utils/navigation';
// Outside Stack
import OnboardingView from '../views/OnboardingView';
import NewServerView from '../views/NewServerView';
import WorkspaceView from '../views/WorkspaceView';
import LoginView from '../views/LoginView';
import ForgotPasswordView from '../views/ForgotPasswordView';
import RegisterView from '../views/RegisterView';
import LegalView from '../views/LegalView';
import AuthenticationWebView from '../views/AuthenticationWebView';
import { ROOT_OUTSIDE } from '../actions/app';
// Outside
const Outside = createStackNavigator();
const _OutsideStack = ({ root }) => {
const { theme } = React.useContext(ThemeContext);
return (
<Outside.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
{root === ROOT_OUTSIDE ? (
<Outside.Screen
name='OnboardingView'
component={OnboardingView}
options={OnboardingView.navigationOptions}
/>
) : null}
<Outside.Screen
name='NewServerView'
component={NewServerView}
options={NewServerView.navigationOptions}
/>
<Outside.Screen
name='WorkspaceView'
component={WorkspaceView}
options={WorkspaceView.navigationOptions}
/>
<Outside.Screen
name='LoginView'
component={LoginView}
options={LoginView.navigationOptions}
/>
<Outside.Screen
name='ForgotPasswordView'
component={ForgotPasswordView}
options={ForgotPasswordView.navigationOptions}
/>
<Outside.Screen
name='RegisterView'
component={RegisterView}
options={RegisterView.navigationOptions}
/>
<Outside.Screen
name='LegalView'
component={LegalView}
options={LegalView.navigationOptions}
/>
</Outside.Navigator>
);
};
const mapStateToProps = state => ({
root: state.app.root
});
_OutsideStack.propTypes = {
root: PropTypes.string
};
const OutsideStack = connect(mapStateToProps)(_OutsideStack);
// OutsideStackModal
const OutsideModal = createStackNavigator();
const OutsideStackModal = () => {
const { theme } = React.useContext(ThemeContext);
return (
<OutsideModal.Navigator mode='modal' screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...ModalAnimation }}>
<OutsideModal.Screen
name='OutsideStack'
component={OutsideStack}
options={{ headerShown: false }}
/>
<OutsideModal.Screen
name='AuthenticationWebView'
component={AuthenticationWebView}
options={AuthenticationWebView.navigationOptions}
/>
</OutsideModal.Navigator>
);
};
export default OutsideStackModal;

View File

@ -1,222 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import { NavigationActions, StackActions } from 'react-navigation';
import KeyCommands from 'react-native-keycommands';
import Navigation from './lib/Navigation';
import { isSplited } from './utils/deviceInfo';
import {
App, RoomContainer, ModalContainer, NotificationContainer
} from './index';
import { MAX_SIDEBAR_WIDTH } from './constants/tablet';
import ModalNavigation from './lib/ModalNavigation';
import { keyCommands, defaultCommands } from './commands';
import { themes } from './constants/colors';
import sharedStyles from './views/Styles';
let modalRef;
let roomRef;
let notificationRef;
export const initTabletNav = (setState) => {
let inCall = false;
const defaultApp = App.router.getStateForAction;
const defaultModal = ModalContainer.router.getStateForAction;
const defaultRoom = RoomContainer.router.getStateForAction;
const defaultNotification = NotificationContainer.router.getStateForAction;
NotificationContainer.router.getStateForAction = (action, state) => {
if (action.type === NavigationActions.NAVIGATE && isSplited()) {
const { routeName, params } = action;
if (routeName === 'RoomView') {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName, params })]
});
roomRef.dispatch(resetAction);
}
}
return defaultNotification(action, state);
};
RoomContainer.router.getStateForAction = (action, state) => {
if (action.type === NavigationActions.NAVIGATE && isSplited()) {
const { routeName, params } = action;
if (routeName === 'RoomActionsView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'AttachmentView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
}
if (action.type === 'Navigation/RESET' && isSplited()) {
const { params } = action.actions[action.index];
const routes = state.routes[state.index] && state.routes[state.index].params;
if (params && params.rid && routes && routes.rid && params.rid === routes.rid) {
return null;
}
}
return defaultRoom(action, state);
};
ModalContainer.router.getStateForAction = (action, state) => {
if (action.type === 'Navigation/POP' && isSplited()) {
modalRef.dispatch(NavigationActions.navigate({ routeName: 'AuthLoading' }));
setState({ showModal: false });
}
if (action.type === NavigationActions.NAVIGATE && isSplited()) {
const { routeName, params } = action;
if (routeName === 'RoomView') {
Navigation.navigate(routeName, params);
}
}
return defaultModal(action, state);
};
App.router.getStateForAction = (action, state) => {
if (action.type === NavigationActions.NAVIGATE) {
const { routeName, params } = action;
if (routeName === 'InsideStack') {
let commands = defaultCommands;
let newState = { inside: true };
if (isSplited()) {
commands = [...commands, ...keyCommands];
newState = { ...newState, showModal: false };
}
KeyCommands.setKeyCommands(commands);
setState(newState);
}
if (isSplited()) {
if (routeName === 'ReadReceiptsView') {
roomRef.dispatch(NavigationActions.navigate({ routeName, params }));
return null;
}
if (routeName === 'OutsideStack') {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
if (routeName === 'JitsiMeetView') {
inCall = true;
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
if (routeName === 'OnboardingView' || routeName === 'NewServerView') {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
if (routeName === 'ModalBlockView' || routeName === 'StatusView' || routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'RoomView') {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName, params })]
});
roomRef.dispatch(resetAction);
notificationRef.dispatch(resetAction);
setState({ showModal: false });
return null;
}
if (routeName === 'RoomsListView') {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'RoomView', params: {} })]
});
roomRef.dispatch(resetAction);
notificationRef.dispatch(resetAction);
setState({ showModal: false });
return null;
}
if (routeName === 'NewMessageView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'DirectoryView') {
modalRef.dispatch(NavigationActions.navigate({ routeName }));
setState({ showModal: true });
return null;
}
}
}
if (action.type === 'Navigation/TOGGLE_DRAWER' && isSplited()) {
modalRef.dispatch(NavigationActions.navigate({ routeName: 'SettingsView' }));
setState({ showModal: true });
return null;
}
if (action.type === 'Navigation/POP' && inCall) {
KeyCommands.setKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: true, showModal: false });
}
return defaultApp(action, state);
};
};
const Split = ({
split, tablet, showModal, closeModal, setModalRef, theme
}) => {
if (split) {
return (
<>
<View style={[sharedStyles.container, sharedStyles.separatorLeft, { borderColor: themes[theme].separatorColor }]}>
<RoomContainer ref={ref => roomRef = ref} screenProps={{ split: tablet, theme }} />
</View>
<ModalContainer showModal={showModal} closeModal={closeModal} ref={setModalRef} screenProps={{ split: tablet, theme }} />
</>
);
}
return null;
};
const Tablet = ({
children, tablet, theme, inside, showModal, closeModal, onLayout
}) => {
const setModalRef = (ref) => {
modalRef = ref;
ModalNavigation.setTopLevelNavigator(modalRef);
};
const split = tablet && inside;
return (
<View style={sharedStyles.containerSplitView} onLayout={onLayout}>
<View style={[sharedStyles.container, split && { maxWidth: MAX_SIDEBAR_WIDTH }]}>
{children}
</View>
<Split split={split} tablet={tablet} theme={theme} showModal={showModal} closeModal={closeModal} setModalRef={setModalRef} />
<NotificationContainer ref={ref => notificationRef = ref} screenProps={{ theme }} />
</View>
);
};
Split.propTypes = {
split: PropTypes.bool,
tablet: PropTypes.bool,
showModal: PropTypes.bool,
closeModal: PropTypes.func,
setModalRef: PropTypes.func,
theme: PropTypes.string
};
Tablet.propTypes = {
children: PropTypes.node,
tablet: PropTypes.bool,
inside: PropTypes.bool,
showModal: PropTypes.bool,
closeModal: PropTypes.func,
onLayout: PropTypes.func,
theme: PropTypes.string
};
export default Tablet;

View File

@ -1,8 +1,6 @@
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info'; import DeviceInfo from 'react-native-device-info';
import { MIN_WIDTH_SPLIT_LAYOUT } from '../constants/tablet';
export const hasNotch = DeviceInfo.hasNotch(); export const hasNotch = DeviceInfo.hasNotch();
export const isIOS = Platform.OS === 'ios'; export const isIOS = Platform.OS === 'ios';
export const isAndroid = !isIOS; export const isAndroid = !isIOS;
@ -18,10 +16,3 @@ export const supportSystemTheme = () => {
// Tablet info // Tablet info
export const isTablet = DeviceInfo.isTablet(); export const isTablet = DeviceInfo.isTablet();
// We need to use this when app is used on splitview with another app
// to handle cases on app view not-larger sufficient to show splited views (room list/room)
// https://github.com/RocketChat/Rocket.Chat.ReactNative/pull/1300#discussion_r341405245
let _width = null;
export const setWidth = width => _width = width;
export const isSplited = () => isTablet && _width > MIN_WIDTH_SPLIT_LAYOUT;

View File

@ -1,8 +1,14 @@
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const navigate = (item) => { const navigate = ({ item, isMasterDetail, ...props }) => {
Navigation.navigate('RoomView', { let navigationMethod = Navigation.navigate;
if (isMasterDetail) {
navigationMethod = Navigation.replace;
}
navigationMethod('RoomView', {
rid: item.rid, rid: item.rid,
name: RocketChat.getRoomTitle(item), name: RocketChat.getRoomTitle(item),
t: item.t, t: item.t,
@ -10,30 +16,32 @@ const navigate = (item) => {
room: item, room: item,
search: item.search, search: item.search,
visitor: item.visitor, visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item) roomUserId: RocketChat.getUidDirectMessage(item),
...props
}); });
}; };
export const goRoom = async(item = {}) => { export const goRoom = async({ item = {}, isMasterDetail = false, ...props }) => {
if (!item.search) { if (item.t === 'd' && item.search) {
return navigate(item);
}
if (item.t === 'd') {
// if user is using the search we need first to join/create room // if user is using the search we need first to join/create room
try { try {
const { username } = item; const { username } = item;
const result = await RocketChat.createDirectMessage(username); const result = await RocketChat.createDirectMessage(username);
if (result.success) { if (result.success) {
return navigate({ return navigate({
item: {
rid: result.room._id, rid: result.room._id,
name: username, name: username,
t: 'd' t: 'd'
},
isMasterDetail,
...props
}); });
} }
} catch { } catch {
// Do nothing // Do nothing
} }
} else {
return navigate(item);
} }
return navigate({ item, isMasterDetail, ...props });
}; };

View File

@ -16,6 +16,11 @@ export const logServerVersion = (serverVersion) => {
}; };
}; };
export const setCurrentScreen = (currentScreen) => {
analytics().setCurrentScreen(currentScreen);
leaveBreadcrumb(currentScreen, { type: 'navigation' });
};
export default (e) => { export default (e) => {
if (e instanceof Error && e.message !== 'Aborted' && !__DEV__) { if (e instanceof Error && e.message !== 'Aborted' && !__DEV__) {
bugsnag.notify(e, (report) => { bugsnag.notify(e, (report) => {

View File

@ -1,50 +0,0 @@
import { StyleSheet } from 'react-native';
import { analytics, leaveBreadcrumb } from './log';
import { themes } from '../constants/colors';
export const defaultHeader = {
headerBackTitle: null
};
export const cardStyle = {
backgroundColor: 'rgba(0,0,0,0)'
};
const borderBottom = theme => ({
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: themes[theme].headerBorder,
elevation: 0
});
export const themedHeader = theme => ({
headerStyle: {
...borderBottom(theme),
backgroundColor: themes[theme].headerBackground
},
headerTintColor: themes[theme].headerTintColor,
headerTitleStyle: { color: themes[theme].headerTitleColor }
});
// gets the current screen from navigation state
export const getActiveRouteName = (navigationState) => {
if (!navigationState) {
return null;
}
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getActiveRouteName(route);
}
return route.routeName;
};
export const onNavigationStateChange = (prevState, currentState) => {
const currentScreen = getActiveRouteName(currentState);
const prevScreen = getActiveRouteName(prevState);
if (prevScreen !== currentScreen) {
analytics().setCurrentScreen(currentScreen);
leaveBreadcrumb(currentScreen, { type: 'navigation' });
}
};

View File

@ -0,0 +1,96 @@
import { Easing, Animated } from 'react-native';
import { TransitionPresets, HeaderStyleInterpolators } from '@react-navigation/stack';
import { isAndroid } from '../deviceInfo';
import conditional from './conditional';
const { multiply } = Animated;
const forFadeFromCenter = ({
current,
closing
}) => {
const opacity = conditional(
closing,
current.progress,
current.progress.interpolate({
inputRange: [0, 0.5, 0.9, 1],
outputRange: [0, 0.25, 0.7, 1]
})
);
return {
cardStyle: {
opacity
}
};
};
const FadeIn = {
animation: 'timing',
config: {
duration: 250,
easing: Easing.out(Easing.poly(5))
}
};
const FadeOut = {
animation: 'timing',
config: {
duration: 150,
easing: Easing.in(Easing.poly(5))
}
};
export const FadeFromCenterModal = {
gestureDirection: 'vertical',
transitionSpec: {
open: FadeIn,
close: FadeOut
},
cardStyleInterpolator: forFadeFromCenter
};
const forStackAndroid = ({
current,
inverted,
layouts: { screen },
closing
}) => {
const translateX = multiply(
current.progress.interpolate({
inputRange: [0, 1],
outputRange: [screen.width, 0]
}),
inverted
);
const opacity = conditional(
closing,
current.progress,
current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1]
})
);
return {
cardStyle: {
opacity,
transform: [{ translateX }]
}
};
};
const StackAndroid = {
gestureDirection: 'horizontal',
transitionSpec: {
open: FadeIn,
close: FadeOut
},
cardStyleInterpolator: forStackAndroid,
headerStyleInterpolator: HeaderStyleInterpolators.forFade
};
export const StackAnimation = isAndroid ? StackAndroid : TransitionPresets.SlideFromRightIOS;
export const ModalAnimation = TransitionPresets.ModalTransition;

View File

@ -0,0 +1,30 @@
// Taken from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/utils/conditional.tsx
import { Animated } from 'react-native';
const { add, multiply } = Animated;
/**
* Use an Animated Node based on a condition. Similar to Reanimated's `cond`.
*
* @param condition Animated Node representing the condition, must be 0 or 1, 1 means `true`, 0 means `false`
* @param main Animated Node to use if the condition is `true`
* @param fallback Animated Node to use if the condition is `false`
*/
export default function conditional(condition, main, fallback) {
// To implement this behavior, we multiply the main node with the condition.
// So if condition is 0, result will be 0, and if condition is 1, result will be main node.
// Then we multiple reverse of the condition (0 if condition is 1) with the fallback.
// So if condition is 0, result will be fallback node, and if condition is 1, result will be 0,
// This way, one of them will always be 0, and other one will be the value we need.
// In the end we add them both together, 0 + value we need = value we need
return add(
multiply(condition, main),
multiply(
condition.interpolate({
inputRange: [0, 1],
outputRange: [1, 0]
}),
fallback
)
);
}

View File

@ -0,0 +1,59 @@
import { StyleSheet } from 'react-native';
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
import { themes } from '../../constants/colors';
export * from './animations';
export const defaultHeader = {
headerBackTitleVisible: false,
cardOverlayEnabled: true,
cardStyle: { backgroundColor: 'transparent' }
};
export const cardStyle = {
backgroundColor: 'rgba(0,0,0,0)'
};
export const borderBottom = theme => ({
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: themes[theme].headerBorder,
elevation: 0
});
export const themedHeader = theme => ({
headerStyle: {
...borderBottom(theme),
backgroundColor: themes[theme].headerBackground
},
headerTintColor: themes[theme].headerTintColor,
headerTitleStyle: { color: themes[theme].headerTitleColor }
});
export const navigationTheme = (theme) => {
const defaultNavTheme = theme === 'light' ? DefaultTheme : DarkTheme;
return {
...defaultNavTheme,
colors: {
...defaultNavTheme.colors,
background: themes[theme].backgroundColor,
border: themes[theme].borderColor
}
};
};
// Gets the current screen from navigation state
export const getActiveRoute = (state) => {
const route = state.routes[state.index];
if (route.state) {
// Dive into nested navigators
return getActiveRoute(route.state);
}
return route;
};
export const getActiveRouteName = state => getActiveRoute(state).name;

View File

@ -1,22 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { WebView } from 'react-native-webview'; import { WebView } from 'react-native-webview';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import I18n from '../../i18n'; import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { DrawerButton } from '../../containers/HeaderButton'; import { DrawerButton } from '../../containers/HeaderButton';
import styles from '../Styles';
import { themedHeader } from '../../utils/navigation';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
class AdminPanelView extends React.Component { class AdminPanelView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({ static navigationOptions = ({ navigation, isMasterDetail }) => ({
...themedHeader(screenProps.theme), headerLeft: isMasterDetail ? undefined : () => <DrawerButton navigation={navigation} />,
headerLeft: <DrawerButton navigation={navigation} />,
title: I18n.t('Admin_Panel') title: I18n.t('Admin_Panel')
}) })
@ -32,7 +28,7 @@ class AdminPanelView extends React.Component {
return null; return null;
} }
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='terms-view'> <SafeAreaView theme={theme}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<WebView <WebView
source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }} source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }}

View File

@ -13,7 +13,6 @@ import EventEmitter from '../utils/events';
import I18n from '../i18n'; import I18n from '../i18n';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import ImageViewer from '../presentation/ImageViewer'; import ImageViewer from '../presentation/ImageViewer';
import { themedHeader } from '../utils/navigation';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { formatAttachmentUrl } from '../lib/utils'; import { formatAttachmentUrl } from '../lib/utils';
import RCActivityIndicator from '../containers/ActivityIndicator'; import RCActivityIndicator from '../containers/ActivityIndicator';
@ -28,26 +27,9 @@ const styles = StyleSheet.create({
}); });
class AttachmentView extends React.Component { class AttachmentView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const { theme } = screenProps;
const attachment = navigation.getParam('attachment');
const from = navigation.getParam('from');
const handleSave = navigation.getParam('handleSave', () => {});
const { title } = attachment;
const options = {
title: decodeURI(title),
...themedHeader(theme),
headerRight: <SaveButton testID='save-image' onPress={handleSave} />
};
if (from !== 'MessagesView') {
options.gesturesEnabled = false;
options.headerLeft = <CloseModalButton testID='close-attachment-view' navigation={navigation} />;
}
return options;
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
@ -58,15 +40,14 @@ class AttachmentView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const attachment = props.navigation.getParam('attachment'); const attachment = props.route.params?.attachment;
this.state = { attachment, loading: true }; this.state = { attachment, loading: true };
this.setHeader();
} }
componentDidMount() { componentDidMount() {
const { navigation } = this.props; const { navigation } = this.props;
navigation.setParams({ handleSave: this.handleSave }); this.unsubscribeBlur = navigation.addListener('blur', () => {
this.willBlurListener = navigation.addListener('willBlur', () => {
if (this.videoRef && this.videoRef.stopAsync) { if (this.videoRef && this.videoRef.stopAsync) {
this.videoRef.stopAsync(); this.videoRef.stopAsync();
} }
@ -74,11 +55,23 @@ class AttachmentView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.willBlurListener && this.willBlurListener.remove) { if (this.unsubscribeBlur) {
this.willBlurListener.remove(); this.unsubscribeBlur();
} }
} }
setHeader = () => {
const { route, navigation } = this.props;
const attachment = route.params?.attachment;
const { title } = attachment;
const options = {
headerLeft: () => <CloseModalButton testID='close-attachment-view' navigation={navigation} />,
title: decodeURI(title),
headerRight: () => <SaveButton testID='save-image' onPress={this.handleSave} />
};
navigation.setOptions(options);
}
getVideoRef = ref => this.videoRef = ref; getVideoRef = ref => this.videoRef = ref;
handleSave = async() => { handleSave = async() => {

View File

@ -2,6 +2,8 @@ import React from 'react';
import { import {
View, Text, StyleSheet, ActivityIndicator View, Text, StyleSheet, ActivityIndicator
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import I18n from '../i18n'; import I18n from '../i18n';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
@ -24,9 +26,7 @@ const styles = StyleSheet.create({
} }
}); });
export default React.memo(withTheme(({ theme, navigation }) => { const AuthLoadingView = React.memo(({ theme, text }) => (
const text = navigation.getParam('text');
return (
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
{text && ( {text && (
@ -36,5 +36,15 @@ export default React.memo(withTheme(({ theme, navigation }) => {
</> </>
)} )}
</View> </View>
); ));
}));
const mapStateToProps = state => ({
text: state.app.text
});
AuthLoadingView.propTypes = {
theme: PropTypes.string,
text: PropTypes.string
};
export default connect(mapStateToProps)(withTheme(AuthLoadingView));

View File

@ -6,29 +6,20 @@ import parse from 'url-parse';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { CloseModalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import ActivityIndicator from '../containers/ActivityIndicator'; import ActivityIndicator from '../containers/ActivityIndicator';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
import { CloseModalButton } from '../containers/HeaderButton';
const userAgent = isIOS const userAgent = isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
: 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36'; : 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36';
class AuthenticationWebView extends React.PureComponent { class AuthenticationWebView extends React.PureComponent {
static navigationOptions = ({ navigation, screenProps }) => {
const authType = navigation.getParam('authType', 'oauth');
return {
...themedHeader(screenProps.theme),
headerLeft: <CloseModalButton navigation={navigation} />,
title: authType === 'saml' || authType === 'cas' ? 'SSO' : 'OAuth'
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
server: PropTypes.string, server: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string
} }
@ -39,7 +30,6 @@ class AuthenticationWebView extends React.PureComponent {
logging: false, logging: false,
loading: false loading: false
}; };
this.authType = props.navigation.getParam('authType', 'oauth');
this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
} }
@ -76,14 +66,15 @@ class AuthenticationWebView extends React.PureComponent {
onNavigationStateChange = (webViewState) => { onNavigationStateChange = (webViewState) => {
const url = decodeURIComponent(webViewState.url); const url = decodeURIComponent(webViewState.url);
if (this.authType === 'saml' || this.authType === 'cas') { const { route } = this.props;
const { navigation } = this.props; const { authType } = route.params;
const ssoToken = navigation.getParam('ssoToken'); if (authType === 'saml' || authType === 'cas') {
const { ssoToken } = route.params;
const parsedUrl = parse(url, true); const parsedUrl = parse(url, true);
// ticket -> cas / validate & saml_idp_credentialToken -> saml // ticket -> cas / validate & saml_idp_credentialToken -> saml
if (parsedUrl.pathname?.includes('validate') || parsedUrl.query?.ticket || parsedUrl.query?.saml_idp_credentialToken) { if (parsedUrl.pathname?.includes('validate') || parsedUrl.query?.ticket || parsedUrl.query?.saml_idp_credentialToken) {
let payload; let payload;
if (this.authType === 'saml') { if (authType === 'saml') {
const token = parsedUrl.query?.saml_idp_credentialToken || ssoToken; const token = parsedUrl.query?.saml_idp_credentialToken || ssoToken;
const credentialToken = { credentialToken: token }; const credentialToken = { credentialToken: token };
payload = { ...credentialToken, saml: true }; payload = { ...credentialToken, saml: true };
@ -94,7 +85,7 @@ class AuthenticationWebView extends React.PureComponent {
} }
} }
if (this.authType === 'oauth') { if (authType === 'oauth') {
if (this.redirectRegex.test(url)) { if (this.redirectRegex.test(url)) {
const parts = url.split('#'); const parts = url.split('#');
const credentials = JSON.parse(parts[1]); const credentials = JSON.parse(parts[1]);
@ -105,13 +96,13 @@ class AuthenticationWebView extends React.PureComponent {
render() { render() {
const { loading } = this.state; const { loading } = this.state;
const { navigation, theme } = this.props; const { route, theme } = this.props;
const uri = navigation.getParam('url'); const { url } = route.params;
return ( return (
<> <>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<WebView <WebView
source={{ uri }} source={{ uri: url }}
userAgent={userAgent} userAgent={userAgent}
onNavigationStateChange={this.onNavigationStateChange} onNavigationStateChange={this.onNavigationStateChange}
onLoadStart={() => { onLoadStart={() => {
@ -131,4 +122,12 @@ const mapStateToProps = state => ({
server: state.server.server server: state.server.server
}); });
AuthenticationWebView.navigationOptions = ({ route, navigation }) => {
const { authType } = route.params;
return {
headerLeft: () => <CloseModalButton navigation={navigation} />,
title: authType === 'saml' || authType === 'cas' ? 'SSO' : 'OAuth'
};
};
export default connect(mapStateToProps)(withTheme(AuthenticationWebView)); export default connect(mapStateToProps)(withTheme(AuthenticationWebView));

View File

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
FlatList, Switch, View, StyleSheet FlatList, Switch, View, StyleSheet, ScrollView
} from 'react-native'; } from 'react-native';
import { SafeAreaView, ScrollView } from 'react-navigation';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -15,7 +14,7 @@ import Separator from '../../containers/Separator';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import SafeAreaView from '../../containers/SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
contentContainerStyle: { contentContainerStyle: {
@ -49,20 +48,19 @@ SectionSeparator.propTypes = {
}; };
class AutoTranslateView extends React.Component { class AutoTranslateView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Auto_Translate'), title: I18n.t('Auto_Translate')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
} }
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
const room = props.navigation.getParam('room'); const room = props.route.params?.room;
if (room && room.observe) { if (room && room.observe) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
@ -163,11 +161,7 @@ class AutoTranslateView extends React.Component {
const { languages } = this.state; const { languages } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView <SafeAreaView testID='auto-translate-view' theme={theme}>
style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}
forceInset={{ vertical: 'never' }}
testID='auto-translate-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { import {
View, Text, Switch, ScrollView, StyleSheet, FlatList View, Text, Switch, ScrollView, StyleSheet, FlatList
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
@ -20,9 +19,9 @@ import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { SWITCH_TRACK_COLOR, themes } from '../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import { Review } from '../utils/review'; import { Review } from '../utils/review';
import { getUserSelector } from '../selectors/login'; import { getUserSelector } from '../selectors/login';
import SafeAreaView from '../containers/SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -73,22 +72,8 @@ const styles = StyleSheet.create({
}); });
class CreateChannelView extends React.Component { class CreateChannelView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = {
const submit = navigation.getParam('submit', () => {}); title: I18n.t('Create_Channel')
const showSubmit = navigation.getParam('showSubmit');
return {
...themedHeader(screenProps.theme),
title: I18n.t('Create_Channel'),
headerRight: (
showSubmit
? (
<CustomHeaderButtons>
<Item title={I18n.t('Create')} onPress={submit} testID='create-channel-submit' />
</CustomHeaderButtons>
)
: null
)
};
} }
static propTypes = { static propTypes = {
@ -114,11 +99,6 @@ class CreateChannelView extends React.Component {
broadcast: false broadcast: false
} }
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ submit: this.submit });
}
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
channelName, type, readOnly, broadcast channelName, type, readOnly, broadcast
@ -148,9 +128,19 @@ class CreateChannelView extends React.Component {
return false; return false;
} }
onChangeText = (channelName) => { toggleRightButton = (channelName) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.setParams({ showSubmit: channelName.trim().length > 0 }); navigation.setOptions({
headerRight: () => channelName.trim().length > 0 && (
<CustomHeaderButtons>
<Item title={I18n.t('Create')} onPress={this.submit} testID='create-channel-submit' />
</CustomHeaderButtons>
)
});
}
onChangeText = (channelName) => {
this.toggleRightButton(channelName);
this.setState({ channelName }); this.setState({ channelName });
} }
@ -294,7 +284,7 @@ class CreateChannelView extends React.Component {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SafeAreaView testID='create-channel-view' style={styles.container} forceInset={{ vertical: 'never' }}> <SafeAreaView testID='create-channel-view' theme={theme}>
<ScrollView {...scrollPersistTaps}> <ScrollView {...scrollPersistTaps}>
<View style={[sharedStyles.separatorVertical, { borderColor: themes[theme].separatorColor }]}> <View style={[sharedStyles.separatorVertical, { borderColor: themes[theme].separatorColor }]}>
<TextInput <TextInput

View File

@ -2,7 +2,6 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ScrollView, Text } from 'react-native'; import { ScrollView, Text } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import Loading from '../../containers/Loading'; import Loading from '../../containers/Loading';
@ -13,7 +12,6 @@ import { CustomHeaderButtons, Item, CloseModalButton } from '../../containers/He
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import TextInput from '../../containers/TextInput'; import TextInput from '../../containers/TextInput';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@ -25,29 +23,13 @@ import SelectChannel from './SelectChannel';
import SelectUsers from './SelectUsers'; import SelectUsers from './SelectUsers';
import styles from './styles'; import styles from './styles';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
class CreateChannelView extends React.Component { class CreateChannelView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const submit = navigation.getParam('submit', () => {});
const showSubmit = navigation.getParam('showSubmit', navigation.getParam('message'));
return {
...themedHeader(screenProps.theme),
title: I18n.t('Create_Discussion'),
headerRight: (
showSubmit
? (
<CustomHeaderButtons>
<Item title={I18n.t('Create')} onPress={submit} testID='create-discussion-submit' />
</CustomHeaderButtons>
)
: null
),
headerLeft: <CloseModalButton navigation={navigation} />
};
}
propTypes = { propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
server: PropTypes.string, server: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
create: PropTypes.func, create: PropTypes.func,
@ -55,31 +37,32 @@ class CreateChannelView extends React.Component {
result: PropTypes.object, result: PropTypes.object,
failure: PropTypes.bool, failure: PropTypes.bool,
error: PropTypes.object, error: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string,
isMasterDetail: PropTypes.bool
} }
constructor(props) { constructor(props) {
super(props); super(props);
const { navigation } = props; const { route } = props;
navigation.setParams({ submit: this.submit }); this.channel = route.params?.channel;
this.channel = navigation.getParam('channel'); const message = route.params?.message ?? {};
const message = navigation.getParam('message', {});
this.state = { this.state = {
channel: this.channel, channel: this.channel,
message, message,
name: message.msg || '', name: message?.msg || '',
users: [], users: [],
reply: '' reply: ''
}; };
this.setHeader();
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { const {
loading, failure, error, result, navigation loading, failure, error, result, isMasterDetail
} = this.props; } = this.props;
if (!isEqual(this.state, prevState)) { if (!isEqual(this.state, prevState)) {
navigation.setParams({ showSubmit: this.valid() }); this.setHeader();
} }
if (!loading && loading !== prevProps.loading) { if (!loading && loading !== prevProps.loading) {
@ -89,17 +72,38 @@ class CreateChannelView extends React.Component {
showErrorAlert(msg); showErrorAlert(msg);
} else { } else {
const { rid, t, prid } = result; const { rid, t, prid } = result;
if (this.channel) { if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
} }
Navigation.navigate('RoomView', { const item = {
rid, name: RocketChat.getRoomTitle(result), t, prid rid, name: RocketChat.getRoomTitle(result), t, prid
}); };
goRoom({ item, isMasterDetail });
} }
}, 300); }, 300);
} }
} }
setHeader = () => {
const { navigation, route } = this.props;
const showCloseModal = route.params?.showCloseModal;
navigation.setOptions({
title: I18n.t('Create_Discussion'),
headerRight: (
this.valid()
? () => (
<CustomHeaderButtons>
<Item title={I18n.t('Create')} onPress={this.submit} testID='create-discussion-submit' />
</CustomHeaderButtons>
)
: null
),
headerLeft: showCloseModal ? () => <CloseModalButton navigation={navigation} /> : undefined
});
}
submit = () => { submit = () => {
const { const {
name: t_name, channel: { prid, rid }, message: { id: pmid }, reply, users name: t_name, channel: { prid, rid }, message: { id: pmid }, reply, users
@ -137,7 +141,7 @@ class CreateChannelView extends React.Component {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SafeAreaView testID='create-discussion-view' style={styles.container} forceInset={{ vertical: 'never' }}> <SafeAreaView testID='create-discussion-view' style={styles.container} theme={theme}>
<ScrollView {...scrollPersistTaps}> <ScrollView {...scrollPersistTaps}>
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Discussion_Desc')}</Text> <Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Discussion_Desc')}</Text>
<SelectChannel <SelectChannel
@ -187,7 +191,8 @@ const mapStateToProps = state => ({
error: state.createDiscussion.error, error: state.createDiscussion.error,
failure: state.createDiscussion.failure, failure: state.createDiscussion.failure,
loading: state.createDiscussion.isFetching, loading: state.createDiscussion.isFetching,
result: state.createDiscussion.result result: state.createDiscussion.result,
isMasterDetail: state.app.isMasterDetail
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View File

@ -3,11 +3,9 @@ import PropTypes from 'prop-types';
import { import {
StyleSheet, FlatList, View, Text, Linking StyleSheet, FlatList, View, Text, Linking
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import I18n from '../i18n'; import I18n from '../i18n';
import { themedHeader } from '../utils/navigation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
@ -17,6 +15,7 @@ import ListItem from '../containers/ListItem';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { DEFAULT_BROWSER_KEY } from '../utils/openLink'; import { DEFAULT_BROWSER_KEY } from '../utils/openLink';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import SafeAreaView from '../containers/SafeAreaView';
const DEFAULT_BROWSERS = [ const DEFAULT_BROWSERS = [
{ {
@ -60,10 +59,9 @@ const styles = StyleSheet.create({
}); });
class DefaultBrowserView extends React.Component { class DefaultBrowserView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Default_browser'), title: I18n.t('Default_browser')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
theme: PropTypes.string theme: PropTypes.string
@ -164,11 +162,7 @@ class DefaultBrowserView extends React.Component {
const { supported } = this.state; const { supported } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView <SafeAreaView testID='default-browser-view' theme={theme}>
style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}
forceInset={{ vertical: 'never' }}
testID='default-browser-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
data={DEFAULT_BROWSERS.concat(supported)} data={DEFAULT_BROWSERS.concat(supported)}

View File

@ -4,7 +4,6 @@ import {
View, FlatList, Text View, FlatList, Text
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@ -22,17 +21,17 @@ import Options from './Options';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
class DirectoryView extends React.Component { class DirectoryView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, isMasterDetail }) => {
const options = { const options = {
...themedHeader(screenProps.theme),
title: I18n.t('Directory') title: I18n.t('Directory')
}; };
if (screenProps.split) { if (isMasterDetail) {
options.headerLeft = <CloseModalButton navigation={navigation} testID='directory-view-close' />; options.headerLeft = () => <CloseModalButton navigation={navigation} testID='directory-view-close' />;
} }
return options; return options;
} }
@ -46,7 +45,8 @@ class DirectoryView extends React.Component {
token: PropTypes.string token: PropTypes.string
}), }),
theme: PropTypes.string, theme: PropTypes.string,
directoryDefaultView: PropTypes.string directoryDefaultView: PropTypes.string,
isMasterDetail: PropTypes.bool
}; };
constructor(props) { constructor(props) {
@ -125,14 +125,14 @@ class DirectoryView extends React.Component {
this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown })); this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown }));
} }
goRoom = async({ goRoom = (item) => {
rid, name, t, search const { navigation, isMasterDetail } = this.props;
}) => { if (isMasterDetail) {
const { navigation } = this.props; navigation.navigate('DrawerNavigator');
await navigation.navigate('RoomsListView'); } else {
navigation.navigate('RoomView', { navigation.navigate('RoomsListView');
rid, name, t, search }
}); goRoom({ item, isMasterDetail });
} }
onPressItem = async(item) => { onPressItem = async(item) => {
@ -230,7 +230,11 @@ class DirectoryView extends React.Component {
} = this.state; } = this.state;
const { isFederationEnabled, theme } = this.props; const { isFederationEnabled, theme } = this.props;
return ( return (
<SafeAreaView style={[styles.safeAreaView, { backgroundColor: themes[theme].backgroundColor }]} testID='directory-view' forceInset={{ vertical: 'never' }}> <SafeAreaView
style={{ backgroundColor: themes[theme].backgroundColor }}
testID='directory-view'
theme={theme}
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
data={data} data={data}
@ -267,7 +271,8 @@ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
isFederationEnabled: state.settings.FEDERATION_Enabled, isFederationEnabled: state.settings.FEDERATION_Enabled,
directoryDefaultView: state.settings.Accounts_Directory_DefaultView directoryDefaultView: state.settings.Accounts_Directory_DefaultView,
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(DirectoryView)); export default connect(mapStateToProps)(withTheme(DirectoryView));

View File

@ -3,9 +3,6 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
export default StyleSheet.create({ export default StyleSheet.create({
safeAreaView: {
flex: 1
},
list: { list: {
flex: 1 flex: 1
}, },

View File

@ -11,17 +11,12 @@ import I18n from '../i18n';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { themedHeader } from '../utils/navigation';
import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../containers/FormContainer';
class ForgotPasswordView extends React.Component { class ForgotPasswordView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ route }) => ({
const title = navigation.getParam('title', 'Rocket.Chat'); title: route.params?.title ?? 'Rocket.Chat'
return { })
title,
...themedHeader(screenProps.theme)
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,

View File

@ -18,14 +18,16 @@ const styles = StyleSheet.create({
} }
}); });
const ForwardLivechatView = ({ forwardRoom, navigation, theme }) => { const ForwardLivechatView = ({
forwardRoom, navigation, route, theme
}) => {
const [departments, setDepartments] = useState([]); const [departments, setDepartments] = useState([]);
const [departmentId, setDepartment] = useState(); const [departmentId, setDepartment] = useState();
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [userId, setUser] = useState(); const [userId, setUser] = useState();
const [room, setRoom] = useState(); const [room, setRoom] = useState();
const rid = navigation.getParam('rid'); const rid = route.params?.rid;
const getDepartments = async() => { const getDepartments = async() => {
try { try {
@ -137,6 +139,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, theme }) => {
ForwardLivechatView.propTypes = { ForwardLivechatView.propTypes = {
forwardRoom: PropTypes.func, forwardRoom: PropTypes.func,
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };
ForwardLivechatView.navigationOptions = { ForwardLivechatView.navigationOptions = {

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ScrollView, View } from 'react-native'; import { ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
@ -17,8 +16,8 @@ import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import Separator from '../../containers/Separator'; import Separator from '../../containers/Separator';
import SafeAreaView from '../../containers/SafeAreaView';
const OPTIONS = { const OPTIONS = {
days: [{ days: [{
@ -60,13 +59,13 @@ const OPTIONS = {
}; };
class InviteUsersView extends React.Component { class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Invite_users'), title: I18n.t('Invite_users')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
timeDateFormat: PropTypes.string, timeDateFormat: PropTypes.string,
createInviteLink: PropTypes.func, createInviteLink: PropTypes.func,
@ -75,7 +74,7 @@ class InviteUsersView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
} }
onValueChangePicker = (key, value) => { onValueChangePicker = (key, value) => {
@ -111,7 +110,7 @@ class InviteUsersView extends React.Component {
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}> <SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} theme={theme}>
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}

View File

@ -3,9 +3,6 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
export default StyleSheet.create({ export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: { innerContainer: {
paddingHorizontal: 20 paddingHorizontal: 20
}, },

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Share, ScrollView } from 'react-native'; import { View, Share, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -18,16 +17,16 @@ import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import SafeAreaView from '../../containers/SafeAreaView';
class InviteUsersView extends React.Component { class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Invite_users'), title: I18n.t('Invite_users')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
timeDateFormat: PropTypes.string, timeDateFormat: PropTypes.string,
invite: PropTypes.object, invite: PropTypes.object,
@ -37,7 +36,7 @@ class InviteUsersView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
} }
componentDidMount() { componentDidMount() {
@ -100,7 +99,7 @@ class InviteUsersView extends React.Component {
theme, invite theme, invite
} = this.props; } = this.props;
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}> <SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} theme={theme}>
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}

View File

@ -1,9 +1,6 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: { innerContainer: {
padding: 20, padding: 20,
paddingBottom: 0 paddingBottom: 0

View File

@ -16,6 +16,7 @@ const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => (
class JitsiMeetView extends React.Component { class JitsiMeetView extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
@ -27,14 +28,14 @@ class JitsiMeetView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
this.onConferenceTerminated = this.onConferenceTerminated.bind(this); this.onConferenceTerminated = this.onConferenceTerminated.bind(this);
this.onConferenceJoined = this.onConferenceJoined.bind(this); this.onConferenceJoined = this.onConferenceJoined.bind(this);
this.jitsiTimeout = null; this.jitsiTimeout = null;
} }
componentDidMount() { componentDidMount() {
const { navigation, user, baseUrl } = this.props; const { route, user, baseUrl } = this.props;
const { const {
name: displayName, id: userId, token, username name: displayName, id: userId, token, username
} = user; } = user;
@ -47,8 +48,8 @@ class JitsiMeetView extends React.Component {
displayName, displayName,
avatar avatar
}; };
const url = navigation.getParam('url'); const url = route.params?.url;
const onlyAudio = navigation.getParam('onlyAudio', false); const onlyAudio = route.params?.onlyAudio ?? false;
if (onlyAudio) { if (onlyAudio) {
JitsiMeet.audioCall(url, userInfo); JitsiMeet.audioCall(url, userInfo);
} else { } else {

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList } from 'react-native'; import { FlatList } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -16,10 +15,10 @@ import ListItem from '../../containers/ListItem';
import Separator from '../../containers/Separator'; import Separator from '../../containers/Separator';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import { appStart as appStartAction, ROOT_LOADING, ROOT_INSIDE } from '../../actions/app';
import { appStart as appStartAction } from '../../actions';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import database from '../../lib/database'; import database from '../../lib/database';
import SafeAreaView from '../../containers/SafeAreaView';
const LANGUAGES = [ const LANGUAGES = [
{ {
@ -59,10 +58,9 @@ const LANGUAGES = [
]; ];
class LanguageView extends React.Component { class LanguageView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Change_Language'), title: I18n.t('Change_Language')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
user: PropTypes.object, user: PropTypes.object,
@ -105,12 +103,12 @@ class LanguageView extends React.Component {
const { appStart } = this.props; const { appStart } = this.props;
await appStart('loading', I18n.t('Change_language_loading')); await appStart({ root: ROOT_LOADING, text: I18n.t('Change_language_loading') });
// shows loading for at least 300ms // shows loading for at least 300ms
await Promise.all([this.changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]); await Promise.all([this.changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]);
await appStart('inside'); await appStart({ root: ROOT_INSIDE });
} }
changeLanguage = async(language) => { changeLanguage = async(language) => {
@ -175,11 +173,7 @@ class LanguageView extends React.Component {
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView <SafeAreaView testID='language-view' theme={theme}>
style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}
forceInset={{ vertical: 'never' }}
testID='language-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
data={LANGUAGES} data={LANGUAGES}
@ -205,7 +199,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
setUser: params => dispatch(setUserAction(params)), setUser: params => dispatch(setUserAction(params)),
appStart: (...params) => dispatch(appStartAction(...params)) appStart: params => dispatch(appStartAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView));

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { import {
Text, ScrollView, View, StyleSheet Text, ScrollView, View, StyleSheet
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
@ -15,12 +14,9 @@ import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import openLink from '../utils/openLink'; import openLink from '../utils/openLink';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation'; import SafeAreaView from '../containers/SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {
flex: 1
},
scroll: { scroll: {
marginTop: 35, marginTop: 35,
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
@ -52,11 +48,6 @@ Separator.propTypes = {
}; };
class LegalView extends React.Component { class LegalView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Legal'),
...themedHeader(screenProps.theme)
})
static propTypes = { static propTypes = {
server: PropTypes.string, server: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string
@ -88,14 +79,7 @@ class LegalView extends React.Component {
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView <SafeAreaView testID='legal-view' theme={theme}>
style={[
styles.container,
{ backgroundColor: themes[theme].auxiliaryBackground }
]}
forceInset={{ vertical: 'never' }}
testID='legal-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
@ -120,4 +104,8 @@ const mapStateToProps = state => ({
server: state.server.server server: state.server.server
}); });
LegalView.navigationOptions = {
title: I18n.t('Legal')
};
export default connect(mapStateToProps)(withTheme(LegalView)); export default connect(mapStateToProps)(withTheme(LegalView));

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, StyleSheet, ScrollView } from 'react-native'; import { Text, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
@ -18,6 +17,7 @@ import scrollPersistTaps from '../utils/scrollPersistTaps';
import { getUserSelector } from '../selectors/login'; import { getUserSelector } from '../selectors/login';
import Chips from '../containers/UIKit/MultiSelect/Chips'; import Chips from '../containers/UIKit/MultiSelect/Chips';
import Button from '../containers/Button'; import Button from '../containers/Button';
import SafeAreaView from '../containers/SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -36,15 +36,17 @@ Title.propTypes = {
theme: PropTypes.string theme: PropTypes.string
}; };
const LivechatEditView = ({ user, navigation, theme }) => { const LivechatEditView = ({
user, navigation, route, theme
}) => {
const [customFields, setCustomFields] = useState({}); const [customFields, setCustomFields] = useState({});
const [availableUserTags, setAvailableUserTags] = useState([]); const [availableUserTags, setAvailableUserTags] = useState([]);
const params = {}; const params = {};
const inputs = {}; const inputs = {};
const livechat = navigation.getParam('room', {}); const livechat = route.params?.room ?? {};
const visitor = navigation.getParam('roomUser', {}); const visitor = route.params?.roomUser ?? {};
const getCustomFields = async() => { const getCustomFields = async() => {
const result = await RocketChat.getCustomFields(); const result = await RocketChat.getCustomFields();
@ -148,8 +150,8 @@ const LivechatEditView = ({ user, navigation, theme }) => {
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps}> <ScrollView {...scrollPersistTaps} style={styles.container}>
<SafeAreaView style={[sharedStyles.container, styles.container]} forceInset={{ vertical: 'never' }}> <SafeAreaView theme={theme}>
<Title <Title
title={visitor?.username} title={visitor?.username}
theme={theme} theme={theme}
@ -271,6 +273,7 @@ const LivechatEditView = ({ user, navigation, theme }) => {
LivechatEditView.propTypes = { LivechatEditView.propTypes = {
user: PropTypes.object, user: PropTypes.object,
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };
LivechatEditView.navigationOptions = ({ LivechatEditView.navigationOptions = ({

View File

@ -13,7 +13,6 @@ import I18n from '../i18n';
import { LegalButton } from '../containers/HeaderButton'; import { LegalButton } from '../containers/HeaderButton';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import { loginRequest as loginRequestAction } from '../actions/login'; import { loginRequest as loginRequestAction } from '../actions/login';
@ -51,14 +50,10 @@ const styles = StyleSheet.create({
}); });
class LoginView extends React.Component { class LoginView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ route, navigation }) => ({
const title = navigation.getParam('title', 'Rocket.Chat'); title: route.params?.title ?? 'Rocket.Chat',
return { headerRight: () => <LegalButton testID='login-view-more' navigation={navigation} />
...themedHeader(screenProps.theme), })
title,
headerRight: <LegalButton testID='login-view-more' navigation={navigation} />
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
@ -205,11 +200,11 @@ class LoginView extends React.Component {
} }
render() { render() {
const { Accounts_ShowFormLogin, theme } = this.props; const { Accounts_ShowFormLogin, theme, navigation } = this.props;
return ( return (
<FormContainer theme={theme} testID='login-view'> <FormContainer theme={theme} testID='login-view'>
<FormContainerInner> <FormContainerInner>
<LoginServices separator={Accounts_ShowFormLogin} /> <LoginServices separator={Accounts_ShowFormLogin} navigation={navigation} />
{this.renderUserForm()} {this.renderUserForm()}
</FormContainerInner> </FormContainerInner>
</FormContainer> </FormContainer>

View File

@ -6,23 +6,21 @@ import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
class MarkdownTableView extends React.Component { class MarkdownTableView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
...themedHeader(screenProps.theme),
title: I18n.t('Table') title: I18n.t('Table')
}); }
static propTypes = { static propTypes = {
navigation: PropTypes.object, route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
} }
render() { render() {
const { navigation, theme } = this.props; const { route, theme } = this.props;
const renderRows = navigation.getParam('renderRows'); const renderRows = route.params?.renderRows;
const tableWidth = navigation.getParam('tableWidth'); const tableWidth = route.params?.tableWidth;
if (isIOS) { if (isIOS) {
return ( return (

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text } from 'react-native'; import { FlatList, View, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import ActionSheet from 'react-native-action-sheet'; import ActionSheet from 'react-native-action-sheet';
@ -15,26 +14,24 @@ import StatusBar from '../../containers/StatusBar';
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage'; import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
const ACTION_INDEX = 0; const ACTION_INDEX = 0;
const CANCEL_INDEX = 1; const CANCEL_INDEX = 1;
class MessagesView extends React.Component { class MessagesView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({ static navigationOptions = ({ route }) => ({
title: I18n.t(navigation.state.params.name), title: I18n.t(route.params?.name)
...themedHeader(screenProps.theme)
}); });
static propTypes = { static propTypes = {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string
split: PropTypes.bool
} }
constructor(props) { constructor(props) {
@ -44,9 +41,9 @@ class MessagesView extends React.Component {
messages: [], messages: [],
fileLoading: true fileLoading: true
}; };
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
this.t = props.navigation.getParam('t'); this.t = props.route.params?.t;
this.content = this.defineMessagesViewContent(props.navigation.getParam('name')); this.content = this.defineMessagesViewContent(props.route.params?.name);
} }
componentDidMount() { componentDidMount() {
@ -223,12 +220,8 @@ class MessagesView extends React.Component {
} }
showAttachment = (attachment) => { showAttachment = (attachment) => {
const { navigation, split } = this.props; const { navigation } = this.props;
let params = { attachment }; navigation.navigate('AttachmentView', { attachment });
if (split) {
params = { ...params, from: 'MessagesView' };
}
navigation.navigate('AttachmentView', params);
} }
onLongPress = (message) => { onLongPress = (message) => {
@ -295,12 +288,9 @@ class MessagesView extends React.Component {
return ( return (
<SafeAreaView <SafeAreaView
style={[ style={{ backgroundColor: themes[theme].backgroundColor }}
styles.list,
{ backgroundColor: themes[theme].backgroundColor }
]}
forceInset={{ vertical: 'never' }}
testID={this.content.testID} testID={this.content.testID}
theme={theme}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
@ -322,4 +312,4 @@ const mapStateToProps = state => ({
customEmojis: state.customEmojis customEmojis: state.customEmojis
}); });
export default connect(mapStateToProps)(withSplit(withTheme(MessagesView))); export default connect(mapStateToProps)(withTheme(MessagesView));

View File

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton'; import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
@ -57,41 +56,18 @@ const mapElementToState = ({ element, blockId, elements = [] }) => {
const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }); const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] });
class ModalBlockView extends React.Component { class ModalBlockView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ route }) => {
const { theme, closeModal } = screenProps; const data = route.params?.data;
const data = navigation.getParam('data');
const cancel = navigation.getParam('cancel', () => {});
const submitting = navigation.getParam('submitting', false);
const { view } = data; const { view } = data;
const { title, submit, close } = view; const { title } = view;
return { return {
title: textParser([title]), title: textParser([title])
...themedHeader(theme),
headerLeft: close ? (
<CustomHeaderButtons>
<Item
title={textParser([close.text])}
style={styles.submit}
onPress={!submitting && (() => cancel({ closeModal }))}
testID='close-modal-uikit'
/>
</CustomHeaderButtons>
) : null,
headerRight: submit ? (
<CustomHeaderButtons>
<Item
title={textParser([submit.text])}
style={styles.submit}
onPress={!submitting && (navigation.getParam('submit', () => {}))}
testID='submit-modal-uikit'
/>
</CustomHeaderButtons>
) : null
}; };
} }
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
user: PropTypes.shape({ user: PropTypes.shape({
@ -103,21 +79,18 @@ class ModalBlockView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.submitting = false; this.submitting = false;
const { navigation } = props; const data = props.route.params?.data;
const data = navigation.getParam('data');
this.values = data.view.blocks.filter(filterInputFields).map(mapElementToState).reduce(reduceState, {}); this.values = data.view.blocks.filter(filterInputFields).map(mapElementToState).reduce(reduceState, {});
this.state = { this.state = {
data, data,
loading: false loading: false
}; };
this.setHeader();
} }
componentDidMount() { componentDidMount() {
const { data } = this.state; const { data } = this.state;
const { navigation } = this.props;
const { viewId } = data; const { viewId } = data;
navigation.setParams({ submit: this.submit, cancel: this.cancel });
EventEmitter.addEventListener(viewId, this.handleUpdate); EventEmitter.addEventListener(viewId, this.handleUpdate);
} }
@ -133,9 +106,9 @@ class ModalBlockView extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { navigation } = this.props; const { navigation, route } = this.props;
const oldData = prevProps.navigation.getParam('data', {}); const oldData = prevProps.route.params?.data ?? {};
const newData = navigation.getParam('data', {}); const newData = route.params?.data ?? {};
if (oldData.viewId !== newData.viewId) { if (oldData.viewId !== newData.viewId) {
navigation.push('ModalBlockView', { data: newData }); navigation.push('ModalBlockView', { data: newData });
} }
@ -147,14 +120,43 @@ class ModalBlockView extends React.Component {
EventEmitter.removeListener(viewId, this.handleUpdate); EventEmitter.removeListener(viewId, this.handleUpdate);
} }
handleUpdate = ({ type, ...data }) => { setHeader = () => {
const { data } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
const { view } = data;
const { title, close, submit } = view;
navigation.setOptions({
title: textParser([title]),
headerLeft: close ? () => (
<CustomHeaderButtons>
<Item
title={textParser([close.text])}
style={styles.submit}
onPress={this.cancel}
testID='close-modal-uikit'
/>
</CustomHeaderButtons>
) : null,
headerRight: submit ? () => (
<CustomHeaderButtons>
<Item
title={textParser([submit.text])}
style={styles.submit}
onPress={this.submit}
testID='submit-modal-uikit'
/>
</CustomHeaderButtons>
) : null
});
}
handleUpdate = ({ type, ...data }) => {
if ([MODAL_ACTIONS.ERRORS].includes(type)) { if ([MODAL_ACTIONS.ERRORS].includes(type)) {
const { errors } = data; const { errors } = data;
this.setState({ errors }); this.setState({ errors });
} else { } else {
this.setState({ data }); this.setState({ data });
navigation.setParams({ data }); this.setHeader();
} }
}; };
@ -187,8 +189,11 @@ class ModalBlockView extends React.Component {
submit = async() => { submit = async() => {
const { data } = this.state; const { data } = this.state;
const { navigation } = this.props; if (this.submitting) {
navigation.setParams({ submitting: true }); return;
}
this.submitting = true;
const { appId, viewId } = data; const { appId, viewId } = data;
this.setState({ loading: true }); this.setState({ loading: true });
@ -207,7 +212,7 @@ class ModalBlockView extends React.Component {
// do nothing // do nothing
} }
navigation.setParams({ submitting: false }); this.submitting = false;
this.setState({ loading: false }); this.setState({ loading: false });
}; };

View File

@ -4,7 +4,6 @@ import {
View, StyleSheet, FlatList, Text View, StyleSheet, FlatList, Text
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
@ -22,16 +21,13 @@ import { CloseModalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import { getUserSelector } from '../selectors/login'; import { getUserSelector } from '../selectors/login';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { createChannelRequest } from '../actions/createChannel'; import { createChannelRequest } from '../actions/createChannel';
import { goRoom } from '../utils/goRoom'; import { goRoom } from '../utils/goRoom';
import SafeAreaView from '../containers/SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeAreaView: {
flex: 1
},
separator: { separator: {
marginLeft: 60 marginLeft: 60
}, },
@ -54,9 +50,8 @@ const styles = StyleSheet.create({
}); });
class NewMessageView extends React.Component { class NewMessageView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({ static navigationOptions = ({ navigation }) => ({
...themedHeader(screenProps.theme), headerLeft: () => <CloseModalButton navigation={navigation} testID='new-message-view-close' />,
headerLeft: <CloseModalButton navigation={navigation} testID='new-message-view-close' />,
title: I18n.t('New_Message') title: I18n.t('New_Message')
}) })
@ -69,7 +64,8 @@ class NewMessageView extends React.Component {
}), }),
createChannel: PropTypes.func, createChannel: PropTypes.func,
maxUsers: PropTypes.number, maxUsers: PropTypes.number,
theme: PropTypes.string theme: PropTypes.string,
isMasterDetail: PropTypes.bool
}; };
constructor(props) { constructor(props) {
@ -150,6 +146,14 @@ class NewMessageView extends React.Component {
}); });
} }
goRoom = (item) => {
const { isMasterDetail, navigation } = this.props;
if (isMasterDetail) {
navigation.pop();
}
goRoom({ item, isMasterDetail });
}
renderButton = ({ renderButton = ({
onPress, testID, title, icon, first onPress, testID, title, icon, first
}) => { }) => {
@ -226,7 +230,7 @@ class NewMessageView extends React.Component {
<UserItem <UserItem
name={item.search ? item.name : item.fname} name={item.search ? item.name : item.fname}
username={item.search ? item.username : item.name} username={item.search ? item.username : item.name}
onPress={() => goRoom(item)} onPress={() => this.goRoom(item)}
baseUrl={baseUrl} baseUrl={baseUrl}
testID={`new-message-view-item-${ item.name }`} testID={`new-message-view-item-${ item.name }`}
style={style} style={style}
@ -256,11 +260,7 @@ class NewMessageView extends React.Component {
render = () => { render = () => {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView <SafeAreaView testID='new-message-view' theme={theme}>
style={[styles.safeAreaView, { backgroundColor: themes[theme].auxiliaryBackground }]}
forceInset={{ vertical: 'never' }}
testID='new-message-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
{this.renderList()} {this.renderList()}
</SafeAreaView> </SafeAreaView>
@ -269,6 +269,7 @@ class NewMessageView extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server, baseUrl: state.server.server,
maxUsers: state.settings.DirectMesssage_maxUsers || 1, maxUsers: state.settings.DirectMesssage_maxUsers || 1,
user: getUserSelector(state) user: getUserSelector(state)

View File

@ -12,10 +12,7 @@ import { encode } from 'base-64';
import parse from 'url-parse'; import parse from 'url-parse';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { import { selectServerRequest, serverRequest } from '../actions/server';
selectServerRequest, serverRequest, serverInitAdd, serverFinishAdd
} from '../actions/server';
import { appStart as appStartAction } from '../actions';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import Button from '../containers/Button'; import Button from '../containers/Button';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
@ -28,7 +25,6 @@ import log from '../utils/log';
import { animateNextTransition } from '../utils/layoutAnimation'; import { animateNextTransition } from '../utils/layoutAnimation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch'; import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
import { themedHeader } from '../utils/navigation';
import { CloseModalButton } from '../containers/HeaderButton'; import { CloseModalButton } from '../containers/HeaderButton';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -65,14 +61,8 @@ const styles = StyleSheet.create({
}); });
class NewServerView extends React.Component { class NewServerView extends React.Component {
static navigationOptions = ({ screenProps, navigation }) => { static navigationOptions = {
const previousServer = navigation.getParam('previousServer', null); title: I18n.t('Workspaces')
const close = navigation.getParam('close', () => {});
return {
headerLeft: previousServer ? <CloseModalButton navigation={navigation} onPress={close} testID='new-server-view-close' /> : undefined,
title: I18n.t('Workspaces'),
...themedHeader(screenProps.theme)
};
} }
static propTypes = { static propTypes = {
@ -81,15 +71,17 @@ class NewServerView extends React.Component {
connecting: PropTypes.bool.isRequired, connecting: PropTypes.bool.isRequired,
connectServer: PropTypes.func.isRequired, connectServer: PropTypes.func.isRequired,
selectServer: PropTypes.func.isRequired, selectServer: PropTypes.func.isRequired,
currentServer: PropTypes.string, adding: PropTypes.bool,
initAdd: PropTypes.func, previousServer: PropTypes.string
finishAdd: PropTypes.func
} }
constructor(props) { constructor(props) {
super(props); super(props);
this.previousServer = props.navigation.getParam('previousServer'); if (props.adding) {
props.navigation.setParams({ close: this.close, previousServer: this.previousServer }); props.navigation.setOptions({
headerLeft: () => <CloseModalButton navigation={props.navigation} onPress={this.close} testID='new-server-view-close' />
});
}
// Cancel // Cancel
this.options = [I18n.t('Cancel')]; this.options = [I18n.t('Cancel')];
@ -108,21 +100,14 @@ class NewServerView extends React.Component {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
} }
componentDidMount() {
const { initAdd } = this.props;
if (this.previousServer) {
initAdd();
}
}
componentWillUnmount() { componentWillUnmount() {
EventEmitter.removeListener('NewServer', this.handleNewServerEvent); EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }
handleBackPress = () => { handleBackPress = () => {
const { navigation } = this.props; const { navigation, previousServer } = this.props;
if (navigation.isFocused() && this.previousServer) { if (navigation.isFocused() && previousServer) {
this.close(); this.close();
return true; return true;
} }
@ -134,11 +119,8 @@ class NewServerView extends React.Component {
} }
close = () => { close = () => {
const { selectServer, currentServer, finishAdd } = this.props; const { selectServer, previousServer } = this.props;
if (this.previousServer !== currentServer) { selectServer(previousServer);
selectServer(this.previousServer);
}
finishAdd();
} }
handleNewServerEvent = (event) => { handleNewServerEvent = (event) => {
@ -344,15 +326,14 @@ class NewServerView extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
connecting: state.server.connecting connecting: state.server.connecting,
adding: state.server.adding,
previousServer: state.server.previousServer
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)), connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)),
initAdd: () => dispatch(serverInitAdd()), selectServer: server => dispatch(selectServerRequest(server))
finishAdd: () => dispatch(serverFinishAdd()),
selectServer: server => dispatch(selectServerRequest(server)),
appStart: root => dispatch(appStartAction(root))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView));

View File

@ -3,7 +3,6 @@ import {
View, ScrollView, Switch, Text View, ScrollView, Switch, Text
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-navigation';
import database from '../../lib/database'; import database from '../../lib/database';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
@ -13,11 +12,10 @@ import Separator from '../../containers/Separator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import SafeAreaView from '../../containers/SafeAreaView';
const SectionTitle = React.memo(({ title, theme }) => ( const SectionTitle = React.memo(({ title, theme }) => (
<Text <Text
@ -140,21 +138,21 @@ const OPTIONS = {
}; };
class NotificationPreferencesView extends React.Component { class NotificationPreferencesView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Notification_Preferences'), title: I18n.t('Notification_Preferences')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
const room = props.navigation.getParam('room'); const room = props.route.params?.room;
this.state = { this.state = {
room: room || {} room: room || {}
}; };
@ -245,7 +243,7 @@ class NotificationPreferencesView extends React.Component {
const { room } = this.state; const { room } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView style={sharedStyles.container} testID='notification-preference-view' forceInset={{ vertical: 'never' }}> <SafeAreaView testID='notification-preference-view' theme={theme}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import { appStart as appStartAction } from '../../actions'; import { appStart as appStartAction, ROOT_BACKGROUND } from '../../actions/app';
import I18n from '../../i18n'; import I18n from '../../i18n';
import Button from '../../containers/Button'; import Button from '../../containers/Button';
import styles from './styles'; import styles from './styles';
@ -16,9 +16,9 @@ import { withTheme } from '../../theme';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
class OnboardingView extends React.Component { class OnboardingView extends React.Component {
static navigationOptions = () => ({ static navigationOptions = {
header: null headerShown: false
}) };
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
@ -28,12 +28,23 @@ class OnboardingView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
if (!isTablet) { if (!isTablet) {
Orientation.lockToPortrait(); Orientation.lockToPortrait();
} }
} }
componentDidMount() {
const { navigation } = this.props;
this.unsubscribeFocus = navigation.addListener('focus', () => {
this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
});
this.unsubscribeBlur = navigation.addListener('blur', () => {
if (this.backHandler && this.backHandler.remove) {
this.backHandler.remove();
}
});
}
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { theme } = this.props; const { theme } = this.props;
if (theme !== nextProps.theme) { if (theme !== nextProps.theme) {
@ -43,12 +54,17 @@ class OnboardingView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); if (this.unsubscribeFocus) {
this.unsubscribeFocus();
}
if (this.unsubscribeBlur) {
this.unsubscribeBlur();
}
} }
handleBackPress = () => { handleBackPress = () => {
const { appStart } = this.props; const { appStart } = this.props;
appStart('background'); appStart({ root: ROOT_BACKGROUND });
return false; return false;
} }
@ -98,7 +114,7 @@ class OnboardingView extends React.Component {
} }
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
appStart: root => dispatch(appStartAction(root)) appStart: params => dispatch(appStartAction(params))
}); });
export default connect(null, mapDispatchToProps)(withTheme(OnboardingView)); export default connect(null, mapDispatchToProps)(withTheme(OnboardingView));

View File

@ -5,7 +5,6 @@ import {
} from 'react-native'; } from 'react-native';
import I18n from '../i18n'; import I18n from '../i18n';
import { themedHeader } from '../utils/navigation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
@ -57,29 +56,29 @@ Item.propTypes = {
}; };
class PickerView extends React.PureComponent { class PickerView extends React.PureComponent {
static navigationOptions = ({ navigation, screenProps }) => ({ static navigationOptions = ({ route }) => ({
title: navigation.getParam('title', I18n.t('Select_an_option')), title: route.params?.title ?? I18n.t('Select_an_option')
...themedHeader(screenProps.theme)
}) })
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
} }
constructor(props) { constructor(props) {
super(props); super(props);
const data = props.navigation.getParam('data', []); const data = props.route.params?.data ?? [];
const value = props.navigation.getParam('value'); const value = props.route.params?.value;
this.state = { data, value }; this.state = { data, value };
this.onSearch = props.navigation.getParam('onChangeText'); this.onSearch = props.route.params?.onChangeText;
} }
onChangeValue = (value) => { onChangeValue = (value) => {
const { navigation } = this.props; const { navigation, route } = this.props;
const goBack = navigation.getParam('goBack', true); const goBack = route.params?.goBack ?? true;
const onChange = navigation.getParam('onChangeValue', () => {}); const onChange = route.params?.onChangeValue ?? (() => {});
onChange(value); onChange(value);
if (goBack) { if (goBack) {
navigation.goBack(); navigation.goBack();

View File

@ -6,8 +6,6 @@ import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256'; import SHA256 from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import { SafeAreaView } from 'react-navigation';
import { HeaderBackButton } from 'react-navigation-stack';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
@ -30,22 +28,19 @@ import { DrawerButton } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
class ProfileView extends React.Component { class ProfileView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({ static navigationOptions = ({ navigation, isMasterDetail }) => {
...themedHeader(screenProps.theme), const options = {
headerLeft: screenProps.split ? (
<HeaderBackButton
onPress={() => navigation.navigate('SettingsView')}
tintColor={themes[screenProps.theme].headerTintColor}
/>
) : (
<DrawerButton navigation={navigation} />
),
title: I18n.t('Profile') title: I18n.t('Profile')
}) };
if (!isMasterDetail) {
options.headerLeft = () => <DrawerButton navigation={navigation} />;
}
return options;
}
static propTypes = { static propTypes = {
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
@ -440,7 +435,7 @@ class ProfileView extends React.Component {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SafeAreaView style={sharedStyles.container} testID='profile-view' forceInset={{ vertical: 'never' }}> <SafeAreaView testID='profile-view' theme={theme}>
<ScrollView <ScrollView
contentContainerStyle={sharedStyles.containerScrollView} contentContainerStyle={sharedStyles.containerScrollView}
testID='profile-view-list' testID='profile-view-list'

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text } from 'react-native'; import { FlatList, View, Text } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -13,18 +12,17 @@ import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
class ReadReceiptView extends React.Component { class ReadReceiptView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Read_Receipt'), title: I18n.t('Read_Receipt')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, route: PropTypes.object,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
@ -33,7 +31,7 @@ class ReadReceiptView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.messageId = props.navigation.getParam('messageId'); this.messageId = props.route.params?.messageId;
this.state = { this.state = {
loading: false, loading: false,
receipts: [] receipts: []
@ -135,11 +133,7 @@ class ReadReceiptView extends React.Component {
} }
return ( return (
<SafeAreaView <SafeAreaView testID='read-receipt-view' theme={theme}>
style={[styles.container, { backgroundColor: themes[theme].chatComponentBackground }]}
forceInset={{ bottom: 'always' }}
testID='read-receipt-view'
>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<View> <View>
{loading {loading

View File

@ -28,9 +28,6 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
padding: 10 padding: 10
}, },
container: {
flex: 1
},
list: { list: {
...sharedStyles.separatorVertical, ...sharedStyles.separatorVertical,
marginVertical: 10 marginVertical: 10

View File

@ -13,7 +13,6 @@ import I18n from '../i18n';
import { LegalButton } from '../containers/HeaderButton'; import { LegalButton } from '../containers/HeaderButton';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import isValidEmail from '../utils/isValidEmail'; import isValidEmail from '../utils/isValidEmail';
@ -53,14 +52,10 @@ const styles = StyleSheet.create({
}); });
class RegisterView extends React.Component { class RegisterView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ route, navigation }) => ({
const title = navigation.getParam('title', 'Rocket.Chat'); title: route.params?.title ?? 'Rocket.Chat',
return { headerRight: () => <LegalButton testID='register-view-more' navigation={navigation} />
...themedHeader(screenProps.theme), });
title,
headerRight: <LegalButton navigation={navigation} testID='register-view-more' />
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
@ -228,11 +223,11 @@ class RegisterView extends React.Component {
render() { render() {
const { saving } = this.state; const { saving } = this.state;
const { theme, showLoginButton } = this.props; const { theme, showLoginButton, navigation } = this.props;
return ( return (
<FormContainer theme={theme} testID='register-view'> <FormContainer theme={theme} testID='register-view'>
<FormContainerInner> <FormContainerInner>
<LoginServices /> <LoginServices navigation={navigation} />
<Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Sign_Up')}</Text> <Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Sign_Up')}</Text>
<TextInput <TextInput
label='Name' label='Name'

View File

@ -4,7 +4,6 @@ import {
View, SectionList, Text, Alert, Share View, SectionList, Text, Alert, Share
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import _ from 'lodash'; import _ from 'lodash';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
@ -24,20 +23,19 @@ import DisclosureIndicator from '../../containers/DisclosureIndicator';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { CloseModalButton } from '../../containers/HeaderButton'; import { CloseModalButton } from '../../containers/HeaderButton';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import SafeAreaView from '../../containers/SafeAreaView';
class RoomActionsView extends React.Component { class RoomActionsView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, isMasterDetail }) => {
const options = { const options = {
...themedHeader(screenProps.theme),
title: I18n.t('Actions') title: I18n.t('Actions')
}; };
if (screenProps.split) { if (isMasterDetail) {
options.headerLeft = <CloseModalButton navigation={navigation} testID='room-actions-view-close' />; options.headerLeft = () => <CloseModalButton navigation={navigation} testID='room-actions-view-close' />;
} }
return options; return options;
} }
@ -45,6 +43,7 @@ class RoomActionsView extends React.Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
@ -59,10 +58,10 @@ class RoomActionsView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false; this.mounted = false;
const room = props.navigation.getParam('room'); const room = props.route.params?.room;
const member = props.navigation.getParam('member'); const member = props.route.params?.member;
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
this.t = props.navigation.getParam('t'); this.t = props.route.params?.t;
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
membersCount: 0, membersCount: 0,
@ -647,7 +646,7 @@ class RoomActionsView extends React.Component {
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView style={styles.container} testID='room-actions-view' forceInset={{ vertical: 'never' }}> <SafeAreaView testID='room-actions-view' theme={theme}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SectionList <SectionList
contentContainerStyle={[styles.contentContainer, { backgroundColor: themes[theme].auxiliaryBackground }]} contentContainerStyle={[styles.contentContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}

View File

@ -4,7 +4,6 @@ import {
Text, View, ScrollView, TouchableOpacity, Keyboard, Alert Text, View, ScrollView, TouchableOpacity, Keyboard, Alert
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -27,11 +26,11 @@ import random from '../../utils/random';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { themedHeader } from '../../utils/navigation';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { MessageTypeValues } from '../../utils/messageTypes'; import { MessageTypeValues } from '../../utils/messageTypes';
import SafeAreaView from '../../containers/SafeAreaView';
const PERMISSION_SET_READONLY = 'set-readonly'; const PERMISSION_SET_READONLY = 'set-readonly';
const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly'; const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
@ -49,13 +48,12 @@ const PERMISSIONS_ARRAY = [
]; ];
class RoomInfoEditView extends React.Component { class RoomInfoEditView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = {
title: I18n.t('Room_Info_Edit'), title: I18n.t('Room_Info_Edit')
...themedHeader(screenProps.theme) }
})
static propTypes = { static propTypes = {
navigation: PropTypes.object, route: PropTypes.object,
deleteRoom: PropTypes.func, deleteRoom: PropTypes.func,
serverVersion: PropTypes.string, serverVersion: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string
@ -101,8 +99,8 @@ class RoomInfoEditView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
loadRoom = async() => { loadRoom = async() => {
const { navigation } = this.props; const { route } = this.props;
const rid = navigation.getParam('rid', null); const rid = route.params?.rid;
if (!rid) { if (!rid) {
return; return;
} }
@ -349,12 +347,16 @@ class RoomInfoEditView extends React.Component {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SafeAreaView
testID='room-info-edit-view'
theme={theme}
style={{ backgroundColor: themes[theme].backgroundColor }}
>
<ScrollView <ScrollView
contentContainerStyle={sharedStyles.containerScrollView} contentContainerStyle={sharedStyles.containerScrollView}
testID='room-info-edit-view-list' testID='room-info-edit-view-list'
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView style={sharedStyles.container} testID='room-info-edit-view' forceInset={{ vertical: 'never' }}>
<RCTextInput <RCTextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
label={I18n.t('Name')} label={I18n.t('Name')}
@ -542,8 +544,8 @@ class RoomInfoEditView extends React.Component {
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<Loading visible={saving} /> <Loading visible={saving} />
</SafeAreaView>
</ScrollView> </ScrollView>
</SafeAreaView>
</KeyboardView> </KeyboardView>
); );
} }

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native'; import { View, Text, ScrollView } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import UAParser from 'ua-parser-js'; import UAParser from 'ua-parser-js';
import _ from 'lodash'; import _ from 'lodash';
@ -16,21 +15,21 @@ import sharedStyles from '../Styles';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomHeaderButtons } from '../../containers/HeaderButton'; import { CustomHeaderButtons, CloseModalButton } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
import Navigation from '../../lib/Navigation';
import Livechat from './Livechat'; import Livechat from './Livechat';
import Channel from './Channel'; import Channel from './Channel';
import Item from './Item'; import Item from './Item';
import Direct from './Direct'; import Direct from './Direct';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation';
const PERMISSION_EDIT_ROOM = 'edit-room'; const PERMISSION_EDIT_ROOM = 'edit-room';
const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd' const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd'
@ -50,50 +49,29 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
); );
class RoomInfoView extends React.Component { class RoomInfoView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const t = navigation.getParam('t');
const rid = navigation.getParam('rid');
const room = navigation.getParam('room');
const roomUser = navigation.getParam('roomUser');
const showEdit = navigation.getParam('showEdit', t === 'l');
return {
title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'),
...themedHeader(screenProps.theme),
headerRight: showEdit
? (
<CustomHeaderButtons>
<Item
iconName='edit'
onPress={() => navigation.navigate(t === 'l' ? 'LivechatEditView' : 'RoomInfoEditView', { rid, room, roomUser })}
testID='room-info-view-edit-button'
/>
</CustomHeaderButtons>
)
: null
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
rooms: PropTypes.array, rooms: PropTypes.array,
split: PropTypes.bool, theme: PropTypes.string,
theme: PropTypes.string isMasterDetail: PropTypes.bool
} }
constructor(props) { constructor(props) {
super(props); super(props);
const room = props.navigation.getParam('room'); const room = props.route.params?.room;
const roomUser = props.navigation.getParam('member'); const roomUser = props.route.params?.member;
this.rid = props.navigation.getParam('rid'); this.rid = props.route.params?.rid;
this.t = props.navigation.getParam('t'); this.t = props.route.params?.t;
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
roomUser: roomUser || {} roomUser: roomUser || {},
showEdit: false
}; };
} }
@ -103,9 +81,10 @@ class RoomInfoView extends React.Component {
} else { } else {
this.loadRoom(); this.loadRoom();
} }
this.setHeader();
const { navigation } = this.props; const { navigation } = this.props;
this.willFocusListener = navigation.addListener('willFocus', () => { this.unsubscribeFocus = navigation.addListener('focus', () => {
if (this.isLivechat) { if (this.isLivechat) {
this.loadVisitor(); this.loadVisitor();
} }
@ -116,11 +95,34 @@ class RoomInfoView extends React.Component {
if (this.subscription && this.subscription.unsubscribe) { if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if (this.willFocusListener && this.willFocusListener.remove) { if (this.unsubscribeFocus) {
this.willFocusListener.remove(); this.unsubscribeFocus();
} }
} }
setHeader = () => {
const { roomUser, room, showEdit } = this.state;
const { navigation, route } = this.props;
const t = route.params?.t;
const rid = route.params?.rid;
const showCloseModal = route.params?.showCloseModal;
navigation.setOptions({
headerLeft: showCloseModal ? () => <CloseModalButton navigation={navigation} /> : undefined,
title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'),
headerRight: showEdit
? () => (
<CustomHeaderButtons>
<Item
iconName='edit'
onPress={() => navigation.navigate(t === 'l' ? 'LivechatEditView' : 'RoomInfoEditView', { rid, room, roomUser })}
testID='room-info-view-edit-button'
/>
</CustomHeaderButtons>
)
: null
});
}
get isDirect() { get isDirect() {
const { room } = this.state; const { room } = this.state;
return room.t === 'd'; return room.t === 'd';
@ -147,8 +149,6 @@ class RoomInfoView extends React.Component {
loadVisitor = async() => { loadVisitor = async() => {
const { room } = this.state; const { room } = this.state;
const { navigation } = this.props;
try { try {
const result = await RocketChat.getVisitorInfo(room?.visitor?._id); const result = await RocketChat.getVisitorInfo(room?.visitor?._id);
if (result.success) { if (result.success) {
@ -159,8 +159,7 @@ class RoomInfoView extends React.Component {
visitor.os = `${ ua.getOS().name } ${ ua.getOS().version }`; visitor.os = `${ ua.getOS().name } ${ ua.getOS().version }`;
visitor.browser = `${ ua.getBrowser().name } ${ ua.getBrowser().version }`; visitor.browser = `${ ua.getBrowser().name } ${ ua.getBrowser().version }`;
} }
this.setState({ roomUser: visitor }); this.setState({ roomUser: visitor }, () => this.setHeader());
navigation.setParams({ roomUser: visitor });
} }
} catch (error) { } catch (error) {
// Do nothing // Do nothing
@ -195,14 +194,13 @@ class RoomInfoView extends React.Component {
} }
loadRoom = async() => { loadRoom = async() => {
const { navigation } = this.props; const { route } = this.props;
let room = navigation.getParam('room'); let room = route.params?.room;
if (room && room.observe) { if (room && room.observe) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
this.subscription = this.roomObservable this.subscription = this.roomObservable
.subscribe((changes) => { .subscribe((changes) => {
this.setState({ room: changes }); this.setState({ room: changes }, () => this.setHeader());
navigation.setParams({ room: changes });
}); });
} else { } else {
try { try {
@ -218,7 +216,7 @@ class RoomInfoView extends React.Component {
const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) { if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
navigation.setParams({ showEdit: true }); this.setState({ showEdit: true }, () => this.setHeader());
} }
} }
@ -236,19 +234,8 @@ class RoomInfoView extends React.Component {
goRoom = () => { goRoom = () => {
const { roomUser, room } = this.state; const { roomUser, room } = this.state;
const { name, username } = roomUser; const { name, username } = roomUser;
const { rooms, navigation, split } = this.props; const { rooms, navigation, isMasterDetail } = this.props;
const params = {
if (room.rid) {
let navigate = navigation.push;
// if this is a room focused
if (rooms.includes(room.rid)) {
({ navigate } = navigation);
} else if (split) {
({ navigate } = Navigation);
}
navigate('RoomView', {
rid: room.rid, rid: room.rid,
name: RocketChat.getRoomTitle({ name: RocketChat.getRoomTitle({
t: room.t, t: room.t,
@ -257,7 +244,21 @@ class RoomInfoView extends React.Component {
}), }),
t: room.t, t: room.t,
roomUserId: RocketChat.getUidDirectMessage(room) roomUserId: RocketChat.getUidDirectMessage(room)
}); };
if (room.rid) {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
if (rooms.includes(room.rid)) {
({ navigate } = navigation);
}
navigate('RoomView', params);
}
} }
} }
@ -327,8 +328,8 @@ class RoomInfoView extends React.Component {
<ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}> <ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<SafeAreaView <SafeAreaView
style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} style={{ backgroundColor: themes[theme].backgroundColor }}
forceInset={{ vertical: 'never' }} theme={theme}
testID='room-info-view' testID='room-info-view'
> >
<View style={[styles.avatarContainer, this.isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}> <View style={[styles.avatarContainer, this.isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}>
@ -346,7 +347,8 @@ class RoomInfoView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
rooms: state.room.rooms rooms: state.room.rooms,
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withSplit(withTheme(RoomInfoView))); export default connect(mapStateToProps)(withTheme(RoomInfoView));

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { FlatList, View } from 'react-native'; import { FlatList, View } from 'react-native';
import ActionSheet from 'react-native-action-sheet'; import ActionSheet from 'react-native-action-sheet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
@ -22,30 +21,17 @@ import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
class RoomMembersView extends React.Component { class RoomMembersView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const toggleStatus = navigation.getParam('toggleStatus', () => {});
const allUsers = navigation.getParam('allUsers');
const toggleText = allUsers ? I18n.t('Online') : I18n.t('All');
return {
title: I18n.t('Members'),
...themedHeader(screenProps.theme),
headerRight: (
<CustomHeaderButtons>
<Item title={toggleText} onPress={toggleStatus} testID='room-members-view-toggle-status' />
</CustomHeaderButtons>
)
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object,
rid: PropTypes.string, rid: PropTypes.string,
members: PropTypes.array, members: PropTypes.array,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
@ -54,7 +40,8 @@ class RoomMembersView extends React.Component {
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
theme: PropTypes.string theme: PropTypes.string,
isMasterDetail: PropTypes.bool
} }
constructor(props) { constructor(props) {
@ -63,8 +50,8 @@ class RoomMembersView extends React.Component {
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1; this.MUTE_INDEX = 1;
this.actionSheetOptions = ['']; this.actionSheetOptions = [''];
const { rid } = props.navigation.state.params; const rid = props.route.params?.rid;
const room = props.navigation.getParam('room'); const room = props.route.params?.room;
this.state = { this.state = {
isLoading: false, isLoading: false,
allUsers: false, allUsers: false,
@ -87,15 +74,15 @@ class RoomMembersView extends React.Component {
} }
}); });
} }
this.setHeader();
} }
async componentDidMount() { async componentDidMount() {
this.mounted = true; this.mounted = true;
this.fetchMembers(); this.fetchMembers();
const { navigation } = this.props; const { route } = this.props;
const { rid } = navigation.state.params; const rid = route.params?.rid;
navigation.setParams({ toggleStatus: this.toggleStatus });
this.permissions = await RocketChat.hasPermission(['mute-user'], rid); this.permissions = await RocketChat.hasPermission(['mute-user'], rid);
} }
@ -105,6 +92,20 @@ class RoomMembersView extends React.Component {
} }
} }
setHeader = () => {
const { allUsers } = this.state;
const { navigation } = this.props;
const toggleText = allUsers ? I18n.t('Online') : I18n.t('All');
navigation.setOptions({
title: I18n.t('Members'),
headerRight: () => (
<CustomHeaderButtons>
<Item title={toggleText} onPress={this.toggleStatus} testID='room-members-view-toggle-status' />
</CustomHeaderButtons>
)
});
}
onSearchChangeText = protectedFunction((text) => { onSearchChangeText = protectedFunction((text) => {
const { members } = this.state; const { members } = this.state;
let membersFiltered = []; let membersFiltered = [];
@ -122,11 +123,11 @@ class RoomMembersView extends React.Component {
const query = await subsCollection.query(Q.where('name', item.username)).fetch(); const query = await subsCollection.query(Q.where('name', item.username)).fetch();
if (query.length) { if (query.length) {
const [room] = query; const [room] = query;
this.goRoom({ rid: room.rid, name: item.username, room }); this.goRoom(room);
} else { } else {
const result = await RocketChat.createDirectMessage(item.username); const result = await RocketChat.createDirectMessage(item.username);
if (result.success) { if (result.success) {
this.goRoom({ rid: result.room._id, name: item.username }); this.goRoom({ rid: result.room?._id, name: item.username, t: 'd' });
} }
} }
} catch (e) { } catch (e) {
@ -180,7 +181,6 @@ class RoomMembersView extends React.Component {
const { const {
rid, members, isLoading, allUsers, end rid, members, isLoading, allUsers, end
} = this.state; } = this.state;
const { navigation } = this.props;
if (isLoading || end) { if (isLoading || end) {
return; return;
} }
@ -194,19 +194,21 @@ class RoomMembersView extends React.Component {
isLoading: false, isLoading: false,
end: newMembers.length < PAGE_SIZE end: newMembers.length < PAGE_SIZE
}); });
navigation.setParams({ allUsers }); this.setHeader();
} catch (e) { } catch (e) {
log(e); log(e);
this.setState({ isLoading: false }); this.setState({ isLoading: false });
} }
} }
goRoom = async({ rid, name, room }) => { goRoom = (item) => {
const { navigation } = this.props; const { navigation, isMasterDetail } = this.props;
await navigation.popToTop(); if (isMasterDetail) {
navigation.navigate('RoomView', { navigation.navigate('DrawerNavigator');
rid, name, t: 'd', room } else {
}); navigation.popToTop();
}
goRoom({ item, isMasterDetail });
} }
handleMute = async() => { handleMute = async() => {
@ -261,7 +263,7 @@ class RoomMembersView extends React.Component {
} = this.state; } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( return (
<SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ vertical: 'never' }}> <SafeAreaView testID='room-members-view' theme={theme}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
data={filtering ? membersFiltered : members} data={filtering ? membersFiltered : members}
@ -289,7 +291,8 @@ class RoomMembersView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state) user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(RoomMembersView)); export default connect(mapStateToProps)(withTheme(RoomMembersView));

View File

@ -18,7 +18,8 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
marginRight: isAndroid ? 15 : 5, marginRight: isAndroid ? 15 : 5,
marginLeft: isAndroid ? androidMarginLeft : -10 marginLeft: isAndroid ? androidMarginLeft : -10,
justifyContent: 'center'
}, },
titleContainer: { titleContainer: {
alignItems: 'center', alignItems: 'center',
@ -126,7 +127,7 @@ HeaderTitle.propTypes = {
}; };
const Header = React.memo(({ const Header = React.memo(({
title, subtitle, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, roomUserId, theme title, subtitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme
}) => { }) => {
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
@ -143,7 +144,7 @@ const Header = React.memo(({
<TouchableOpacity <TouchableOpacity
testID='room-view-header-actions' testID='room-view-header-actions'
onPress={onPress} onPress={onPress}
style={[styles.container, { width: width - widthOffset }]} style={styles.container}
disabled={tmid} disabled={tmid}
> >
<View style={[styles.titleContainer, tmid && styles.threadContainer]}> <View style={[styles.titleContainer, tmid && styles.threadContainer]}>
@ -173,7 +174,6 @@ Header.propTypes = {
status: PropTypes.string, status: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
widthOffset: PropTypes.number,
connecting: PropTypes.bool, connecting: PropTypes.bool,
roomUserId: PropTypes.string, roomUserId: PropTypes.string,
goRoomActionsView: PropTypes.func goRoomActionsView: PropTypes.func

View File

@ -43,7 +43,7 @@ const Icon = React.memo(({
} else if (type === 'c') { } else if (type === 'c') {
icon = 'hash'; icon = 'hash';
} else if (type === 'l') { } else if (type === 'l') {
icon = 'livechat'; icon = 'omnichannel';
} else if (type === 'd') { } else if (type === 'd') {
icon = 'team'; icon = 'team';
} else { } else {

View File

@ -14,6 +14,7 @@ class RightButtonsContainer extends React.PureComponent {
t: PropTypes.string, t: PropTypes.string,
tmid: PropTypes.string, tmid: PropTypes.string,
navigation: PropTypes.object, navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
toggleFollowThread: PropTypes.func toggleFollowThread: PropTypes.func
}; };
@ -57,9 +58,15 @@ class RightButtonsContainer extends React.PureComponent {
} }
goThreadsView = () => { goThreadsView = () => {
const { rid, t, navigation } = this.props; const {
rid, t, navigation, isMasterDetail
} = this.props;
if (isMasterDetail) {
navigation.navigate('ModalStackNavigator', { screen: 'ThreadMessagesView', params: { rid, t } });
} else {
navigation.navigate('ThreadMessagesView', { rid, t }); navigation.navigate('ThreadMessagesView', { rid, t });
} }
}
toggleFollowThread = () => { toggleFollowThread = () => {
const { isFollowingThread } = this.state; const { isFollowingThread } = this.state;
@ -104,7 +111,8 @@ class RightButtonsContainer extends React.PureComponent {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
userId: getUserSelector(state).id, userId: getUserSelector(state).id,
threadsEnabled: state.settings.Threads_enabled threadsEnabled: state.settings.Threads_enabled,
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(RightButtonsContainer); export default connect(mapStateToProps)(RightButtonsContainer);

View File

@ -1,9 +1,8 @@
import React from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { HeaderBackButton } from 'react-navigation-stack'; import { HeaderBackButton } from '@react-navigation/stack';
import { isIOS } from '../../../utils/deviceInfo';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import Avatar from '../../../containers/Avatar'; import Avatar from '../../../containers/Avatar';
@ -14,19 +13,20 @@ const styles = StyleSheet.create({
} }
}); });
const RoomHeaderLeft = ({ const RoomHeaderLeft = React.memo(({
tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, split tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, isMasterDetail
}) => { }) => {
if (!split || tmid) { if (!isMasterDetail || tmid) {
const onPress = useCallback(() => navigation.goBack());
return ( return (
<HeaderBackButton <HeaderBackButton
title={unreadsCount > 999 ? '+999' : unreadsCount || ' '} label={unreadsCount > 999 ? '+999' : unreadsCount || ' '}
backTitleVisible={isIOS} onPress={onPress}
onPress={() => navigation.goBack()}
tintColor={themes[theme].headerTintColor} tintColor={themes[theme].headerTintColor}
/> />
); );
} }
const onPress = useCallback(() => goRoomActionsView(), []);
if (baseUrl && userId && token) { if (baseUrl && userId && token) {
return ( return (
<Avatar <Avatar
@ -37,12 +37,12 @@ const RoomHeaderLeft = ({
style={styles.avatar} style={styles.avatar}
userId={userId} userId={userId}
token={token} token={token}
onPress={goRoomActionsView} onPress={onPress}
/> />
); );
} }
return null; return null;
}; });
RoomHeaderLeft.propTypes = { RoomHeaderLeft.propTypes = {
tmid: PropTypes.string, tmid: PropTypes.string,
@ -55,7 +55,7 @@ RoomHeaderLeft.propTypes = {
t: PropTypes.string, t: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
goRoomActionsView: PropTypes.func, goRoomActionsView: PropTypes.func,
split: PropTypes.bool isMasterDetail: PropTypes.bool
}; };
export default RoomHeaderLeft; export default RoomHeaderLeft;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { FlatList, InteractionManager, RefreshControl } from 'react-native'; import { FlatList, RefreshControl } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
@ -57,7 +57,7 @@ class List extends React.Component {
animated: false animated: false
}; };
this.init(); this.init();
this.didFocusListener = props.navigation.addListener('didFocus', () => { this.unsubscribeFocus = props.navigation.addListener('focus', () => {
if (this.mounted) { if (this.mounted) {
this.setState({ animated: true }); this.setState({ animated: true });
} else { } else {
@ -106,7 +106,6 @@ class List extends React.Component {
this.unsubscribeMessages(); this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable this.messagesSubscription = this.messagesObservable
.subscribe((data) => { .subscribe((data) => {
this.interaction = InteractionManager.runAfterInteractions(() => {
if (tmid && this.thread) { if (tmid && this.thread) {
data = [this.thread, ...data]; data = [this.thread, ...data];
} }
@ -117,7 +116,6 @@ class List extends React.Component {
this.state.messages = messages; this.state.messages = messages;
} }
}); });
});
} }
} }
@ -157,14 +155,11 @@ class List extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.unsubscribeMessages(); this.unsubscribeMessages();
if (this.interaction && this.interaction.cancel) {
this.interaction.cancel();
}
if (this.onEndReached && this.onEndReached.stop) { if (this.onEndReached && this.onEndReached.stop) {
this.onEndReached.stop(); this.onEndReached.stop();
} }
if (this.didFocusListener && this.didFocusListener.remove) { if (this.unsubscribeFocus) {
this.didFocusListener.remove(); this.unsubscribeFocus();
} }
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }

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