From 06cca9c615152a9c3a2db83d658853e368bca182 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Wed, 19 Feb 2020 17:52:05 -0300 Subject: [PATCH] [NEW] Default browser (#1752) Co-authored-by: Diego Mello --- README.md | 2 +- __mocks__/rn-user-defaults.js | 4 + app/i18n/locales/en.js | 4 + app/i18n/locales/pt-BR.js | 4 + app/index.js | 3 + app/utils/openLink.js | 62 ++++++++++- app/views/DefaultBrowserView.js | 191 ++++++++++++++++++++++++++++++++ app/views/SettingsView/index.js | 24 ++-- ios/RocketChatRN/Info.plist | 7 ++ 9 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 __mocks__/rn-user-defaults.js create mode 100644 app/views/DefaultBrowserView.js diff --git a/README.md b/README.md index c55fa7c9..392aa070 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 00000000..71f26ca3 --- /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 d5bdfcf8..f995fae5 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 c1d7a95d..1729b0bf 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 428a0c19..b9490474 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 9978590a..9341693a 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 00000000..8825ddce --- /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 022b1166..68b4c921 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 47b8a3b6..6f7b07e2 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -103,5 +103,12 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + googlechrome + googlechromes + firefox + brave +