-
-
Notifications
You must be signed in to change notification settings - Fork 672
Open links in external browser #4679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3710763
3d455bd
06b784c
64f25d4
d7dc4f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -263,13 +263,29 @@ export type RealmState = {| | |
| // https://github.com/zulip/zulip-mobile/issues/4009#issuecomment-619280681. | ||
| export type ThemeName = 'default' | 'night'; | ||
|
|
||
| /** | ||
| * The values for this mean: | ||
| * | ||
| * * embedded: The in-app browser | ||
| * * external: The user's default browser app | ||
| * * default: 'external' on iOS, 'embedded' on Android | ||
| * | ||
| * Use the `shouldUseInAppBrowser` function from src/utils/openLink.js in order to | ||
| * parse this. | ||
| * | ||
| * See https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser | ||
| * for the reasoning behind these options. | ||
| */ | ||
| export type BrowserPreference = 'embedded' | 'external' | 'default'; | ||
|
Comment on lines
+266
to
+279
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this detailed jsdoc! Like all our jsdoc, it should also get a one-line summary at the top. It seems I haven't written that down in our style guide, hmm. But when I do, I don't expect to improve on this item and the one after it (just some details are different because it's a different language):
Here, one way of looking at it would be: this nicely provides the answer corresponding to each possible value. But it doesn't really say what question the answers are answering. That'd be a good subject for the summary line. So one version would be: (That also demonstrates a slight reformatting of the list that I think makes it a bit easier to visually parse.) |
||
|
|
||
| export type SettingsState = {| | ||
| locale: string, | ||
| theme: ThemeName, | ||
| offlineNotification: boolean, | ||
| onlineNotification: boolean, | ||
| experimentalFeaturesEnabled: boolean, | ||
| streamNotification: boolean, | ||
| browser: BrowserPreference, | ||
| |}; | ||
|
|
||
| export type StreamsState = Stream[]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,12 +6,13 @@ import { ScrollView } from 'react-native'; | |
| import type { RouteProp } from '../react-navigation'; | ||
| import type { MainTabsNavigationProp } from '../main/MainTabsScreen'; | ||
| import * as NavigationService from '../nav/NavigationService'; | ||
| import type { Dispatch } from '../types'; | ||
| import type { Dispatch, BrowserPreference } from '../types'; | ||
| import { createStyleSheet } from '../styles'; | ||
| import { connect } from '../react-redux'; | ||
| import { getSettings } from '../selectors'; | ||
| import { OptionButton, OptionRow } from '../common'; | ||
| import { | ||
| IconBrowser, | ||
| IconDiagnostics, | ||
| IconNotifications, | ||
| IconNight, | ||
|
|
@@ -25,6 +26,7 @@ import { | |
| navigateToDiagnostics, | ||
| navigateToLegal, | ||
| } from '../actions'; | ||
| import { shouldUseInAppBrowser } from '../utils/openLink'; | ||
|
|
||
| const styles = createStyleSheet({ | ||
| optionWrapper: { | ||
|
|
@@ -37,6 +39,7 @@ type Props = $ReadOnly<{| | |
| route: RouteProp<'settings', void>, | ||
|
|
||
| theme: string, | ||
| browser: BrowserPreference, | ||
| dispatch: Dispatch, | ||
| |}>; | ||
|
|
||
|
|
@@ -47,7 +50,7 @@ class SettingsScreen extends PureComponent<Props> { | |
| }; | ||
|
|
||
| render() { | ||
| const { theme } = this.props; | ||
| const { dispatch, theme, browser } = this.props; | ||
|
|
||
| return ( | ||
| <ScrollView style={styles.optionWrapper}> | ||
|
|
@@ -57,6 +60,14 @@ class SettingsScreen extends PureComponent<Props> { | |
| value={theme === 'night'} | ||
| onValueChange={this.handleThemeChange} | ||
| /> | ||
| <OptionRow | ||
| Icon={IconBrowser} | ||
| label="Open links with in-app browser" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, it looks like This reminds me of a discussion here, with an idea that would make this an error in dev, at least at the point of trying to render this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good catch! A PR to error on untranslated strings in dev sounds great. |
||
| value={shouldUseInAppBrowser(browser)} | ||
| onValueChange={value => { | ||
| dispatch(settingsChange({ browser: value ? 'embedded' : 'external' })); | ||
| }} | ||
| /> | ||
| <OptionButton | ||
| Icon={IconNotifications} | ||
| label="Notifications" | ||
|
|
@@ -92,4 +103,5 @@ class SettingsScreen extends PureComponent<Props> { | |
|
|
||
| export default connect(state => ({ | ||
| theme: getSettings(state).theme, | ||
| browser: getSettings(state).browser, | ||
| }))(SettingsScreen); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ const initialState: SettingsState = { | |
| onlineNotification: true, | ||
| experimentalFeaturesEnabled: false, | ||
| streamNotification: false, | ||
| browser: 'default', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a migration for adding this to the settings state. I was a bit surprised to see things working, in my local testing, without one. It turns out that the It took me a while staring at the code to find that fact, and I'm not aware that we intentionally depend on it anywhere. Also, We've also found that it's good to err on the side of being explicit and literal in what goes in migrations; e.g., in 1193a57. b058fa2 has an example of adding a migration for more data being stored. That one was definitely necessary, I think; the "shallow merge" couldn't have helped there.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
| }; | ||
|
|
||
| export default (state: SettingsState = initialState, action: Action): SettingsState => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,38 @@ | ||
| /* @flow strict-local */ | ||
| import { NativeModules, Platform } from 'react-native'; | ||
| import { NativeModules, Platform, Linking } from 'react-native'; | ||
| import SafariView from 'react-native-safari-view'; | ||
|
|
||
| export default (url: string): void => { | ||
| import type { BrowserPreference, GetState } from '../types'; | ||
| import { getSettings } from '../selectors'; | ||
|
|
||
| export function openLinkEmbedded(url: string): void { | ||
|
gnprice marked this conversation as resolved.
|
||
| if (Platform.OS === 'ios') { | ||
| SafariView.show({ url: encodeURI(url) }); | ||
| } else { | ||
| NativeModules.CustomTabsAndroid.openURL(url); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| export function openLinkExternal(url: string): void { | ||
| Linking.openURL(url); | ||
| } | ||
|
|
||
| export function shouldUseInAppBrowser(browser: BrowserPreference): boolean { | ||
| if (browser === 'default') { | ||
| return Platform.OS === 'android'; | ||
| } else { | ||
| return browser === 'embedded'; | ||
| } | ||
| } | ||
|
|
||
| // TODO: We may want to turn this into a thunk action creator. | ||
| // See https://github.com/zulip/zulip-mobile/pull/4679#discussion_r625991786 | ||
| export function openLinkWithUserPreference(url: string, getState: GetState): void { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a function needs That has the nice benefit that the stuff you want to happen in it is guaranteed to happen after the store has rehydrated; that's because of our use of The trouble here is that we're not in a file like Or, I think I'd be OK merging this as-is with a comment noting that it could one day become a thunk action creator.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (waiting on @gnprice to weigh in here)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I agree that this interface taking Probably the simplest resolution is to have this just take a OTOH if we had more call sites, it would start to feel weird that part of the interface of this function was effectively that you had to say something like Maybe the best solution is a point in between: have it take a
|
||
| const state = getState(); | ||
| const browser = getSettings(state).browser; | ||
| if (shouldUseInAppBrowser(browser)) { | ||
| openLinkEmbedded(url); | ||
| } else { | ||
| openLinkExternal(url); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should get a quick explanation, like the other migrations above. (Chris already took care of this in 74ae528.)