-
-
Notifications
You must be signed in to change notification settings - Fork 678
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
Open links in external browser #4679
Conversation
118070a
to
b7a88f7
Compare
Thanks, @WesleyAC, this LGTM! I tested on iOS and it works as expected. I added one commit on top, to bump the new |
4f3e1b2
to
693f597
Compare
@chrisbobbe Thanks! I added a setting as per the CZO discussion, would be good to get a review on the last commit in this stack :) |
693f597
to
fb197b8
Compare
Ok, pushed a new version with defaults as discussed on CZO :) |
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.
Thanks, @WesleyAC! I'm not sure I've mentioned this yet but I'm quite looking forward to this. I often uninstall and reinstall the app on my iPhone, since debug and release builds share an identifier (you can't have one of each installed, like on Android). That means the embedded browser doesn't have a long memory for me being logged into things; I want to start using my normal cookies, as Greg mentions in his reasoning on CZO. A few comments below.
src/reduxTypes.js
Outdated
// * embedded: The in-app browser | ||
// * external: The user's default browser app | ||
// * default: 'external' on iOS, 'embedded' on Android | ||
// |
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.
nit: Use a jsdoc-style comment to describe interface (I think that's all of the comments you've added here on BrowserPreference
).
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.
Done!
/> | ||
<OptionRow | ||
Icon={IconBrowser} | ||
label="Open links with in-app browser" |
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.
Ah, it looks like OptionRow
(possibly soon to be named SwitchRow
, in #4683?) makes a <Label />
with label
, and Label
does try and look up a translation for the string. So we'll need to put this string in messages_en.json.
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 <OptionRow />
(so in particular you'd have to be looking at the settings screen after this change). Does that sound good to you; should I send a PR?
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.
Ah, good catch! A PR to error on untranslated strings in dev sounds great.
} | ||
} | ||
|
||
export function openLinkWithUserPreference(url: string, getState: GetState): void { |
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.
When a function needs getState
, we've more often made it into a thunk action creator; there are several examples of thunk action creators in fetchActions
, like fetchMessages
.
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 redux-action-buffer
. I think the intent is that we can put a bunch of different kinds of stuff in thunk action creators—stuff that we want to prevent happening before the store has rehydrated, or at least that we don't want to have to reason about it running before then. 42adfb3 has some background on this, and here was a place where I hadn't originally reached for a thunk action creator but Greg mentioned it'd be a good choice.
The trouble here is that we're not in a file like fooActions.js
. @gnprice, is that a barrier to putting any action creators in the file?
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.
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.
(waiting on @gnprice to weigh in here)
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.
Hmm. I agree that this interface taking getState
feels odd. I feel like the direction I'd rather go in is to separate this function from being concerned with our Redux store at all -- that feels to me like a layer that lives above this one.
Probably the simplest resolution is to have this just take a BrowserPreference
. The caller is already looking around at various other bits of the state, and it's no trouble for it to to pick out that one too and pass it here.
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 getSettings(state).browser
in its second argument. The exact bit of settings that this depends on feels like it should be an internal implementation detail.
Maybe the best solution is a point in between: have it take a SettingsState
. Then:
- The exact setting it needs is an internal detail, just like in the current version.
- But it's no longer exposed to the whole Redux state.
- Also the fact that it does depend on some user setting becomes explicit in the interface. I think it's good for that to be explicit -- I wouldn't e.g. want this to take just one argument
url: string
, and go find the Redux store as a global or something, or if it did then I'd at least want that connection to be drawn clearly in its jsdoc.
onlineNotification: true, | ||
experimentalFeaturesEnabled: false, | ||
streamNotification: false, | ||
browser: 'default', |
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.
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 REHYDRATE
action doesn't clobber the initialState
with the state that's just been grabbed from storage. Rather, it does a "shallow merge": https://github.com/zulip/zulip-mobile/blob/v27.162/src/third/redux-persist/autoRehydrate.js#L69. That's why there's still a state.settings.browser
, post-rehydration.
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, redux-persist
and redux-persist-migrate
sort of work together, so this logic could plausibly change as we overhaul redux-persist
.
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.
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.
Done!
fb197b8
to
8064cb1
Compare
Should be ready for another look @chrisbobbe :) |
src/reduxTypes.js
Outdated
/* parse this. | ||
/* | ||
/* See https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser | ||
/* for the reasoning behind these options. |
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.
nit: there are extra /
's on the left; looks like they should be spaces
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.
Fixed :)
8064cb1
to
3c8a1f2
Compare
Co-authored-by: Shaurya Jain <[email protected]>
Co-authored-by: Shaurya Jain <[email protected]>
Fixes: zulip#4323 Co-authored-by: Shaurya Jain <[email protected]>
This will let people choose whether they want to use the in-app browser, or their external browser app. This defaults to enabled on Android, and disabled on iOS. The setting for this could probably be improved - See the discussion [1] on CZO for what this looks like in other apps. I've opted to use a simple switch, since that's what we're already using in this UI, and I don't want to redesign it right now. I expect we will want to redesign it in the future, but given that we have so few settings right now, I'm not too worried about the settings screen being confusing/overwhelming. Another thing that I'd like to improve is the icon - I'm using the Feather icon for Google Chrome, since the only other browser-related icons I could find were the globe icon we're using for the language setting in this same screen, and the compass icon, which is less recognizable than the Chrome one. I'm unhappy with using a branded icon here, but Feather doesn't give us very many options, sadly. [1]: https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1168259
3c8a1f2
to
d7dc4f9
Compare
Added that comment, going ahead and merging. Happy to circle back later if we decide we want a thunk action creator instead. |
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.
Thanks @WesleyAC and @chrisbobbe !
Just took a proper look through these changes today. Comments below, including one on that thunk-action-creator thread and one on the icon.
/** | ||
* 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'; |
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.
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):
https://dart.dev/guides/language/effective-dart/documentation#do-start-doc-comments-with-a-single-sentence-summary
In particular:
Provide just enough context for the reader to orient themselves and decide if they should keep reading or look elsewhere for the solution to their problem.
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:
/**
* The user's preference on what browser to open links with.
*
* The values for this mean:
* * embedded: The in-app browser
* * external: The user's default browser app
* * default: 'external' on iOS, 'embedded' on Android
*
…
(That also demonstrates a slight reformatting of the list that I think makes it a bit easier to visually parse.)
} | ||
} | ||
|
||
export function openLinkWithUserPreference(url: string, getState: GetState): void { |
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.
Hmm. I agree that this interface taking getState
feels odd. I feel like the direction I'd rather go in is to separate this function from being concerned with our Redux store at all -- that feels to me like a layer that lives above this one.
Probably the simplest resolution is to have this just take a BrowserPreference
. The caller is already looking around at various other bits of the state, and it's no trouble for it to to pick out that one too and pass it here.
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 getSettings(state).browser
in its second argument. The exact bit of settings that this depends on feels like it should be an internal implementation detail.
Maybe the best solution is a point in between: have it take a SettingsState
. Then:
- The exact setting it needs is an internal detail, just like in the current version.
- But it's no longer exposed to the whole Redux state.
- Also the fact that it does depend on some user setting becomes explicit in the interface. I think it's good for that to be explicit -- I wouldn't e.g. want this to take just one argument
url: string
, and go find the Redux store as a global or something, or if it did then I'd at least want that connection to be drawn clearly in its jsdoc.
|
||
'28': state => ({ |
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.)
Fixes: #4323
Based on #4343