Skip to content
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

Use auth headers in ImageBackground to get a FallbackAvatarURL. #4367

Merged
merged 3 commits into from
Dec 31, 2020

Conversation

chrisbobbe
Copy link
Contributor

As discussed yesterday, around here.

@chrisbobbe chrisbobbe requested a review from gnprice December 30, 2020 16:03
@chrisbobbe chrisbobbe force-pushed the pr-avatar-auth-headers branch from e672bf6 to e624db3 Compare December 30, 2020 16:06
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spotting and debugging this! Small comments below.

width: size,
borderRadius,
};
const UserAvatar = function UserAvatar(props: Props) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The const UserAvatar = function UserAvatar… form seems a bit redundant -- I'd rather write the name just once. How about

function UserAvatar(props: Props) {

?

I'm choosing a non-arrow function so that I can give the function a
name (the same name as the variable where we store it). I think
React (understandably) has trouble putting a name to arrow functions
and anonymous non-arrow functions in stack traces and in the dev
tools, though I haven't recently tested this.

So I believe we actually have a Babel plugin operating that turns things into this form, given the form with arrow functions we usually use. Namely, @babel/plugin-transform-function-name, which is pulled in by metro-react-native-babel-preset (see node_modules/metro-react-native-babel-preset/src/configs/main.js.)

That arrangement actually seems slightly silly to me -- why not just write something like

function UserAvatar(props: Props) {  }

in the first place? 😛 It's shorter, even, than our usual (for functions) form

const UserAvatar = (props: Props) => {  }

But that arrangement is what we implicitly rely on in a lot of the app.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Dec 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That arrangement actually seems slightly silly to me -- why not just write something like

function UserAvatar(props: Props) {  }

in the first place? 😛

Ah, right—I left this out of my explanation; oops. I have never favored this form because, like with var, it means I have to think about hoisting (doc).

But in this case it's probably fine; React component files aren't generally long (or shouldn't be, anyway). 🙂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that's helpful. It seems quite likely that's been other people's thinking too, and has helped motivate the const foo = () => { … } style.

I think the term "hoisting", which suggests an action of transforming the code into some other code, makes what's going on here more complicated than it needs to be. (You can see some ambivalence about it right at the top of that doc.)

With var there's some surprising semantics because you can have a declaration separate from an initialization, and "hoisting" may be a helpful idea for thinking about those semantics.

But with function, there's no such complication -- a function declaration function foo() { … } just means there's a function, and it's called foo, and you can call it from anywhere under that scope. If it's at module toplevel, you can call it from anywhere in the module.

There's really less to think about with this form than there is with const foo = function() { … } -- the latter form adds the complication that the function only exists in part of the code, namely after the declaration. So you have to put the code in a certain order, but only sometimes (only if you're referring to the function in code directly at top level, outside of any function... or in an IIFE... or more generally in code that can execute before the module's top-level code has completed execution), and you end up with lint rules like no-use-before-define that try to make things simple again by making the constraints tighter, but that make it impossible to write some kinds of code, or to write some other code naturally, and so we disable that rule in several files.

Whereas with a plain function declaration, you just have the function, and you can call it.

I agree that OTOH the complicated semantics of var, which "hoisting" is an attempt to explain, are a good reason to generally avoid using it.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Dec 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's really less to think about with this form than there is with const foo = function() { … }

What do you think of this small tweak to one of the examples in that doc?

catName("Chloe");

const suffix = "the cat"

function catName(name) {
  console.log("My cat's name is " + name + " " + suffix);
}

There, the order does matter—and it might be a bad surprise to someone working with the assumption that they could call catName from anywhere in the same scope. And our automated checks don't catch it; I just tried it out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that is indeed more complicated 🙂 .

I think from the perspective above, the thing that's making that example complicated isn't the function -- it's that there's nontrivial logic happening at toplevel. Because there's that catName call happening at toplevel, it depends on suffix being present, and so it depends on the const suffix = … having already happened -- IOW there's a required order between those two statements. There wouldn't be such an ordering constraint if the catName call were inside a function definition (in any form), and were only invoked via an export.

In general it's a bad practice to have substantial logic happening at module toplevel, because it means that requiring a module can have side effects, or at a minimum can lead to spending time doing computation. I think it's rare in our code -- the only frequent patterns of top-level code that come to mind, beyond function expressions, are a handful of higher-order functions like connect that do no visible work up front.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Dec 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's that there's nontrivial logic happening at toplevel

True, it might be happening at toplevel—but it might not, right;

class IosCompliantAppleAuthButton extends PureComponent<Props, State> {
  state = {
    isNativeButtonAvailable: undefined,
  };

  async componentDidMount() {
    this.setState({ isNativeButtonAvailable: await AppleAuthentication.isAvailableAsync() });
  }

  render() {
    const { style, onPress, theme } = this.props;
    const { isNativeButtonAvailable } = this.state;
+   
+   catName("Chloe");
+
+   const suffix = "the cat"
+   
+   function catName(name) {
+     console.log("My cat's name is " + name + " " + suffix);
+   }
+   
    // [etc.]
  }
}

Though in a case like that, you'd really have to be trying to make the mistake, I guess.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. And I'd look a lot more skeptically at a function declaration inside a function. I guess in fact one way to look at that is:

  • the module toplevel is for definitions, and not logic -- so function statements are fine, and function expressions with long complex bodies (as in const f = (…): … => { … }) are the bread and butter of our codebase, but nontrivial logic to be executed immediately requires care;
  • a function body is for logic -- so function statements, and for that matter complex function expressions, make things complex and require care, but nontrivial logic is at home;
  • and one reason that's good is that mixing definitions and logic at the same level leads to surprises like this one.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Dec 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, right; that's a really helpful way of thinking about it.

);
}
}
const auth = useSelector((state: GlobalState) => getAuth(state));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests we can probably benefit from a simple type-wrapper for useSelector, in our src/react-redux.js -- one that just specifies that the callback's argument is of type GlobalState.

I can try adding that after this is merged (or while merging it.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds good—thanks!

// For `FallbackAvatarURL`s.
headers: getAuthHeaders(auth),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm puzzled now at why this hasn't applied all along to uploaded avatars. Discussion in chat.

chrisbobbe and others added 3 commits December 30, 2020 18:38
I'd like to try using react-redux's `useSelector` instead of
`connect` -- and that means turning this class component into a
function component. This is quite a simple component to convert.

I'm choosing a non-arrow function so that I can give the function a
name (the same name as the variable where we store it). I think
React (understandably) has trouble putting a name to arrow functions
and anonymous non-arrow functions in stack traces and in the dev
tools, though I haven't recently tested this.
It was a mystery how we ever managed to show a FallbackAvatarURL
without sending the auth headers. It turns out that, on my dev
server, React Native was storing a cookie upon getting an OK
response from `api.devFetchApiKey` -- but no such cookie was being
set upon successfully logging in to CZO.

We should not yet consider moving to cookie-based authentication
because it's documented as being quite unstable [0].

See discussion, where, first, we thought it was an issue with not
following the 302 redirect [1]; then, we found that we just needed
to send the auth headers [2]; then, we found out what was going on
with the cookies on my dev server, at least on iOS [3].

[0] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/302.20redirect.20for.20avatar/near/1089706
[1] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/302.20redirect.20for.20avatar/near/1089332
[2] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/302.20redirect.20for.20avatar/near/1089508
[3] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/302.20redirect.20for.20avatar/near/1089697
@gnprice gnprice force-pushed the pr-avatar-auth-headers branch from e624db3 to 602bcc1 Compare December 31, 2020 02:40
@gnprice gnprice merged commit 602bcc1 into zulip:master Dec 31, 2020
@gnprice
Copy link
Member

gnprice commented Dec 31, 2020

Thanks again! Merged, with a useSelector type-wrapper added on top.

@chrisbobbe chrisbobbe deleted the pr-avatar-auth-headers branch December 31, 2020 04:08
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Dec 31, 2020
Greg pointed out (in response to d750645) [1] that the
`const foo = function foo() { ... }` form is repetitive, which it
is.

I'd given just a bit of reasoning for the repetitive form in
a0dcf4f, which I also gave in response to Greg's comment -- but
after more discussion, I was convinced that plain function
declarations (at toplevel, where definitions belong, and nontrivial
logic doesn't) are at least as good and possibly better. We'll fix
a0dcf4f in the next commit.

[1] zulip#4367 (comment)
gnprice pushed a commit to chrisbobbe/zulip-mobile that referenced this pull request Jan 1, 2021
Greg pointed out (in response to d750645) [1] that the
`const foo = function foo() { ... }` form is repetitive, which it
is.

I'd given just a bit of reasoning for the repetitive form in
a0dcf4f, which I also gave in response to Greg's comment -- but
after more discussion, I was convinced that plain function
declarations (at toplevel, where definitions belong, and nontrivial
logic doesn't) are at least as good and possibly better. We'll fix
a0dcf4f in the next commit.

[1] zulip#4367 (comment)
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Jan 13, 2021
See discussion at
  https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/302.20redirect.20for.20avatar/near/1097413.

In zulip#4367, I'd assumed that it would be fine to include the auth
headers even when they're unnecessary. That turns out not to be
true: the s3 backend doesn't want them.

Fixes: zulip#4401
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Jul 20, 2021
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Jul 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants