diff --git a/README.md b/README.md
index c55fa7c96..392aa0700 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ Readme will guide you on how to config.
| Report message | ✅ |
| Theming | ✅ |
| Settings -> Review the App | ✅ |
-| Settings -> Default Browser | ❌ |
+| Settings -> Default Browser | ✅ |
| Admin panel | ✅ |
| Reply message from notification | ✅ |
| Unread counter banner on message list | ✅ |
diff --git a/__mocks__/rn-user-defaults.js b/__mocks__/rn-user-defaults.js
new file mode 100644
index 000000000..71f26ca30
--- /dev/null
+++ b/__mocks__/rn-user-defaults.js
@@ -0,0 +1,4 @@
+export default {
+ set: () => '',
+ get: () => ''
+};
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index d5bdfcf87..f995fae58 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -112,6 +112,7 @@ export default {
Back: 'Back',
Black: 'Black',
Block_user: 'Block user',
+ Browser: 'Browser',
Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
Broadcast_Channel: 'Broadcast Channel',
Busy: 'Busy',
@@ -132,6 +133,7 @@ export default {
Choose: 'Choose',
Choose_from_library: 'Choose from library',
Choose_file: 'Choose file',
+ Choose_where_you_want_links_be_opened: 'Choose where you want links be opened',
Code: 'Code',
Collaborative: 'Collaborative',
Confirm: 'Confirm',
@@ -157,6 +159,7 @@ export default {
Dark: 'Dark',
Dark_level: 'Dark Level',
Default: 'Default',
+ Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
delete: 'delete',
Delete: 'Delete',
@@ -206,6 +209,7 @@ export default {
Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel',
+ In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
Invisible: 'Invisible',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index c1d7a95df..1729b0bf6 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -114,6 +114,7 @@ export default {
Back: 'Voltar',
Black: 'Preto',
Block_user: 'Bloquear usuário',
+ Browser: 'Navegador',
Broadcast_channel_Description: 'Somente usuários autorizados podem escrever novas mensagens, mas os outros usuários poderão responder',
Broadcast_Channel: 'Canal de Transmissão',
Busy: 'Ocupado',
@@ -134,6 +135,7 @@ export default {
Choose: 'Escolher',
Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo',
+ Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
Code: 'Código',
Collaborative: 'Colaborativo',
Confirm: 'Confirmar',
@@ -154,6 +156,7 @@ export default {
Create: 'Criar',
Dark: 'Escuro',
Dark_level: 'Nível escuro',
+ Default_browser: 'Navegador padrão',
Delete_Room_Warning: 'A exclusão de uma sala irá apagar todas as mensagens postadas na sala. Isso não pode ser desfeito.',
delete: 'excluir',
Delete: 'Excluir',
@@ -197,6 +200,7 @@ export default {
Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu da conversa',
+ In_app: 'No app',
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
diff --git a/app/index.js b/app/index.js
index 428a0c190..b9490474f 100644
--- a/app/index.js
+++ b/app/index.js
@@ -217,6 +217,9 @@ const SettingsStack = createStackNavigator({
},
ThemeView: {
getScreen: () => require('./views/ThemeView').default
+ },
+ DefaultBrowserView: {
+ getScreen: () => require('./views/DefaultBrowserView').default
}
}, {
defaultNavigationOptions: defaultHeader,
diff --git a/app/utils/openLink.js b/app/utils/openLink.js
index 9978590ad..9341693a1 100644
--- a/app/utils/openLink.js
+++ b/app/utils/openLink.js
@@ -1,12 +1,62 @@
+import { Linking } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
+import RNUserDefaults from 'rn-user-defaults';
+import parse from 'url-parse';
import { themes } from '../constants/colors';
-const openLink = (url, theme = 'light') => WebBrowser.openBrowserAsync(url, {
- toolbarColor: themes[theme].headerBackground,
- controlsColor: themes[theme].headerTintColor,
- collapseToolbar: true,
- showTitle: true
-});
+export const DEFAULT_BROWSER_KEY = 'DEFAULT_BROWSER_KEY';
+
+const scheme = {
+ chrome: 'googlechrome:',
+ chromeSecure: 'googlechromes:',
+ firefox: 'firefox:',
+ brave: 'brave:'
+};
+
+const appSchemeURL = (url, browser) => {
+ let schemeUrl = url;
+ const parsedUrl = parse(url, true);
+ const { protocol } = parsedUrl;
+ const isSecure = ['https:'].includes(protocol);
+
+ if (browser === 'googlechrome') {
+ if (!isSecure) {
+ schemeUrl = url.replace(protocol, scheme.chrome);
+ } else {
+ schemeUrl = url.replace(protocol, scheme.chromeSecure);
+ }
+ } else if (browser === 'firefox') {
+ schemeUrl = `${ scheme.firefox }//open-url?url=${ url }`;
+ } else if (browser === 'brave') {
+ schemeUrl = `${ scheme.brave }//open-url?url=${ url }`;
+ }
+
+ return schemeUrl;
+};
+
+const openLink = async(url, theme = 'light') => {
+ try {
+ const browser = await RNUserDefaults.get(DEFAULT_BROWSER_KEY);
+
+ if (browser) {
+ const schemeUrl = appSchemeURL(url, browser.replace(':', ''));
+ await Linking.openURL(schemeUrl);
+ } else {
+ await WebBrowser.openBrowserAsync(url, {
+ toolbarColor: themes[theme].headerBackground,
+ controlsColor: themes[theme].headerTintColor,
+ collapseToolbar: true,
+ showTitle: true
+ });
+ }
+ } catch {
+ try {
+ await Linking.openURL(url);
+ } catch {
+ // do nothing
+ }
+ }
+};
export default openLink;
diff --git a/app/views/DefaultBrowserView.js b/app/views/DefaultBrowserView.js
new file mode 100644
index 000000000..8825ddce6
--- /dev/null
+++ b/app/views/DefaultBrowserView.js
@@ -0,0 +1,191 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ StyleSheet, FlatList, View, Text, Linking
+} from 'react-native';
+import { SafeAreaView } from 'react-navigation';
+import RNUserDefaults from 'rn-user-defaults';
+
+import I18n from '../i18n';
+import { themedHeader } from '../utils/navigation';
+import { withTheme } from '../theme';
+import { themes } from '../constants/colors';
+import sharedStyles from './Styles';
+import StatusBar from '../containers/StatusBar';
+import Separator from '../containers/Separator';
+import ListItem from '../containers/ListItem';
+import { CustomIcon } from '../lib/Icons';
+import { DEFAULT_BROWSER_KEY } from '../utils/openLink';
+import { isIOS } from '../utils/deviceInfo';
+
+const DEFAULT_BROWSERS = [
+ {
+ title: I18n.t('In_app'),
+ value: 'inApp'
+ },
+ {
+ title: isIOS ? 'Safari' : I18n.t('Browser'),
+ value: 'systemDefault:'
+ }
+];
+
+const BROWSERS = [
+ {
+ title: 'Chrome',
+ value: 'googlechrome:'
+ },
+ {
+ title: 'Firefox',
+ value: 'firefox:'
+ },
+ {
+ title: 'Brave',
+ value: 'brave:'
+ }
+];
+
+const styles = StyleSheet.create({
+ list: {
+ paddingBottom: 18
+ },
+ info: {
+ paddingTop: 25,
+ paddingBottom: 18,
+ paddingHorizontal: 16
+ },
+ infoText: {
+ fontSize: 16,
+ ...sharedStyles.textRegular
+ }
+});
+
+class DefaultBrowserView extends React.Component {
+ static navigationOptions = ({ screenProps }) => ({
+ title: I18n.t('Default_browser'),
+ ...themedHeader(screenProps.theme)
+ })
+
+ static propTypes = {
+ theme: PropTypes.string
+ }
+
+ state = {
+ browser: null,
+ supported: []
+ }
+
+ constructor(props) {
+ super(props);
+ if (isIOS) {
+ this.init();
+ }
+ }
+
+ async componentDidMount() {
+ this.mounted = true;
+ try {
+ const browser = await RNUserDefaults.get(DEFAULT_BROWSER_KEY);
+ this.setState({ browser });
+ } catch {
+ // do nothing
+ }
+ }
+
+ init = () => {
+ BROWSERS.forEach((browser) => {
+ const { value } = browser;
+ Linking.canOpenURL(value).then((installed) => {
+ if (installed) {
+ if (this.mounted) {
+ this.setState(({ supported }) => ({ supported: [...supported, browser] }));
+ } else {
+ const { supported } = this.state;
+ this.state.supported = [...supported, browser];
+ }
+ }
+ });
+ });
+ }
+
+ isSelected = (value) => {
+ const { browser } = this.state;
+ if (!browser && value === 'inApp') {
+ return true;
+ }
+ return browser === value;
+ }
+
+ changeDefaultBrowser = async(newBrowser) => {
+ try {
+ const browser = newBrowser !== 'inApp' ? newBrowser : null;
+ await RNUserDefaults.set(DEFAULT_BROWSER_KEY, browser);
+ this.setState({ browser });
+ } catch {
+ // do nothing
+ }
+ }
+
+ renderSeparator = () => {
+ const { theme } = this.props;
+ return ;
+ }
+
+ renderIcon = () => {
+ const { theme } = this.props;
+ return ;
+ }
+
+ renderItem = ({ item }) => {
+ const { theme } = this.props;
+ const { title, value } = item;
+ return (
+ this.changeDefaultBrowser(value)}
+ testID={`default-browser-view-${ title }`}
+ right={this.isSelected(value) ? this.renderIcon : null}
+ theme={theme}
+ />
+ );
+ }
+
+ renderHeader = () => {
+ const { theme } = this.props;
+ return (
+ <>
+
+ {I18n.t('Choose_where_you_want_links_be_opened')}
+
+ {this.renderSeparator()}
+ >
+ );
+ }
+
+ render() {
+ const { supported } = this.state;
+ const { theme } = this.props;
+ return (
+
+
+ item.value}
+ contentContainerStyle={[
+ styles.list,
+ { borderColor: themes[theme].separatorColor }
+ ]}
+ renderItem={this.renderItem}
+ ListHeaderComponent={this.renderHeader}
+ ListFooterComponent={this.renderSeparator}
+ ItemSeparatorComponent={this.renderSeparator}
+ />
+
+ );
+ }
+}
+
+export default withTheme(DefaultBrowserView);
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
index 022b11663..68b4c921b 100644
--- a/app/views/SettingsView/index.js
+++ b/app/views/SettingsView/index.js
@@ -138,9 +138,9 @@ class SettingsView extends React.Component {
}
}
- navigateToRoom = (room) => {
+ navigateToScreen = (screen) => {
const { navigation } = this.props;
- navigation.navigate(room);
+ navigation.navigate(screen);
}
sendEmail = async() => {
@@ -175,11 +175,6 @@ class SettingsView extends React.Component {
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
}
- changeTheme = () => {
- const { navigation } = this.props;
- navigation.navigate('ThemeView');
- }
-
onPressLicense = () => {
const { theme } = this.props;
openLink(LICENSE_LINK, theme);
@@ -234,7 +229,7 @@ class SettingsView extends React.Component {
this.navigateToRoom('ProfileView')}
+ onPress={() => this.navigateToScreen('ProfileView')}
showActionIndicator
testID='settings-profile'
right={this.renderDisclosure}
@@ -255,7 +250,7 @@ class SettingsView extends React.Component {
this.navigateToRoom('LanguageView')}
+ onPress={() => this.navigateToScreen('LanguageView')}
showActionIndicator
testID='settings-view-language'
right={this.renderDisclosure}
@@ -280,10 +275,19 @@ class SettingsView extends React.Component {
theme={theme}
/>
+ this.navigateToScreen('DefaultBrowserView')}
+ testID='settings-view-default-browser'
+ right={this.renderDisclosure}
+ theme={theme}
+ />
+
this.navigateToScreen('ThemeView')}
testID='settings-view-theme'
right={this.renderDisclosure}
theme={theme}
diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist
index 47b8a3b63..6f7b07e25 100644
--- a/ios/RocketChatRN/Info.plist
+++ b/ios/RocketChatRN/Info.plist
@@ -103,5 +103,12 @@
UIViewControllerBasedStatusBarAppearance
+ LSApplicationQueriesSchemes
+
+ googlechrome
+ googlechromes
+ firefox
+ brave
+