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

Display presence status taking away status into account #3326

Merged
merged 1 commit into from
Feb 12, 2019

Conversation

borisyankov
Copy link
Contributor

@borisyankov borisyankov commented Feb 4, 2019

Update all presence UI to correctly, taking into account away status.

@gnprice gnprice added the review label Feb 4, 2019
@borisyankov borisyankov added P0 critical Highest priority blocked on other work To come back to after another related PR, or some other task. labels Feb 5, 2019
@gnprice gnprice removed the blocked on other work To come back to after another related PR, or some other task. label Feb 6, 2019
@gnprice
Copy link
Member

gnprice commented Feb 6, 2019

#3263 is now merged, which I believe unblocks this one!

@borisyankov borisyankov force-pushed the presence-avatar branch 7 times, most recently from ac0ae35 to fe6d841 Compare February 7, 2019 13:56
@borisyankov borisyankov changed the title WIP: Display presence status taking away status into account Display presence status taking away status into account Feb 7, 2019
@borisyankov
Copy link
Contributor Author

This is ready for review.

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 @borisyankov !

This looks generally good. I quite like the simplification sweeping through in the second commit!

Specific comments below.

users.reduce((usersById, user) => {
usersById[user.email] = user;
return usersById;
}, ({ ...undefined }: UserEmailMap)),
Copy link
Member

Choose a reason for hiding this comment

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

Huh interesting -- why this instead of {}?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes: "Cannot cast object literal to UserEmailMap because inexact object literal [1] is incompatible with exact UserEmailMap [2]."

Because {} is considered 'unsealed object'
https://flow.org/en/docs/types/objects/#toc-unsealed-objects

A concept in Flow that might be removed as not very intuitive.

Copy link
Member

Choose a reason for hiding this comment

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

Huh fascinating!

Ah, I see you've made UserEmailMap exact. I've shied away from making many types with "indexer properties" exact, because I keep getting strange errors from them. This may be a very useful workaround for that problem!

Let's be sure to mention that either in a comment and/or the commit message. Also you mentioned in audio chat that you found this workaround in an issue thread on Flow. That link will be super useful to have written down.

Copy link
Member

Choose a reason for hiding this comment

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

bump:

Also you mentioned in audio chat that you found this workaround in an issue thread on Flow. That link will be super useful to have written down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The exact issue I found was:
facebook/flow#7343
This does not have any extra information that is worth linking to, besides the fact it named the problem (unsealed objects) which made me read the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are other GitHub issues that discuss sealed/unsealed objects, like:
facebook/flow#2977

I don't think linking them directly is of any specific benefit since the main benefit is identifying the cause and knowing what term to search for.

@@ -41,6 +41,13 @@ export const getAllUsersByEmail = createSelector(getAllUsers, allUsers =>
}, {}),
);

export const getUsersByEmail: Selector<UserEmailMap> = createSelector(getUsers, (users = []) =>
users.reduce((usersById, user) => {
Copy link
Member

Choose a reason for hiding this comment

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

I think I've mentioned before that I'm not a fan of this pattern where reduce is used but the callback actually just mutates the object -- I think they're almost always clearer as a straightforward loop. Because it's just like the code around it, though, I won't press that here; maybe I'll follow up separately with a sweep through to change a lot of these.

@@ -41,6 +41,13 @@ export const getAllUsersByEmail = createSelector(getAllUsers, allUsers =>
}, {}),
);

export const getUsersByEmail: Selector<UserEmailMap> = createSelector(getUsers, (users = []) =>
users.reduce((usersById, user) => {
usersById[user.email] = user;
Copy link
Member

Choose a reason for hiding this comment

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

This name definitely needs to be made accurate, though 😉

Copy link
Member

Choose a reason for hiding this comment

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

bump

Copy link
Member

Choose a reason for hiding this comment

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

bump

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@@ -41,6 +41,13 @@ export const getAllUsersByEmail = createSelector(getAllUsers, allUsers =>
}, {}),
);

export const getUsersByEmail: Selector<UserEmailMap> = createSelector(getUsers, (users = []) =>
Copy link
Member

Choose a reason for hiding this comment

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

The commit message here says:

We should not use the existing `getAllUsersByEmail` since it returns
all users and realm bots and messes up our strict typing.

Two questions:

  • We should not use that selector where? From context I think you probably mean in the upcoming change to PresenceStatusIndicator; that's fine but should be explicit, like "In an upcoming commit, we'll want to use this instead of the existing getAllUsersByEmail because the latter [...]".
    • Remember the context of the next few commits isn't always handy or obvious, when this is found in the history in the future.
    • Without that, it sounds like you're saying we should never use getAllUsersByEmail. Maybe that is what you're saying? I'm not actually 100% sure.
  • Why not use getAllUsersByEmail in that upcoming commit? This new one amounts to a filtered view of that one, with a subset of the entries. Sure, it seems plausible that the difference will never matter... but if it ever does (we're showing presence status for a "user" that doesn't show up in getUsers), it seems like we want to go ahead and use the data. And that's the simpler thing, too.

More broadly, I definitely agree that these should be made clearer. I think the most important bit will be to work out the differences in what they actually mean... and write that down as a sentence or two each, in jsdoc. I'm not entirely sure what the contrasts are myself. Then we can find names that reflect those contrasts.

There are some clues in the commit messages in this file's history -- take a look at git log --stat -p src/users/userSelectors.js.

Copy link
Member

Choose a reason for hiding this comment

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

bump

@@ -2,9 +2,12 @@
/* eslint-disable react-native/no-unused-styles */
import React, { PureComponent } from 'react';
import { StyleSheet, View } from 'react-native';
import { connect } from 'react-redux';
Copy link
Member

Choose a reason for hiding this comment

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

This refactoring makes sense to me! Thanks also for the clear commit messages explaining why ❤️

@@ -25,9 +28,16 @@ const styles = StyleSheet.create({
},
});

type PropsFromState = {|
Copy link
Member

Choose a reason for hiding this comment

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

Ah, interesting idea to split this out! I can think of some other components that would likely benefit from a similar change.

I'm not keen on using the word "state" here -- the thing it first makes me think of in this context (juxtaposed so closely with Props) is the React component's state, which is not at all what you mean. How about PropsFromRedux?

Copy link
Member

Choose a reason for hiding this comment

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

Or perhaps PropsFromConnect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was playing with ConnectedProps but sounded too cutesy and not clear.
PropsFromConnect sounds good. Did rename.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense!

I think I've sometimes said "connected props" and spelled it "connected props"... but without the punctuation it does become unclear.

@@ -38,18 +48,28 @@ type Props = {|
* * gray if 'offline'
*
* @prop [style] - Style object for additional customization.
* @prop [presence] - UserPresence object used to determine the status from.
* @prop [email] - email of the user whose status we are showing.
Copy link
Member

Choose a reason for hiding this comment

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

This is no longer optional, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. Fixed.


const awayStatus = userStatus[user.user_id];

const status = awayStatus && awayStatus.away ? 'offline' : statusFromPresence(userPresence);
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I think I'd rather this logic be encapsulated in its own function or method somewhere.

A minimal change would be to put it in a method on the component -- so like

render() {
  const status = this.getStatus();
  if (status === null) {
    return null;
  }

  const { style, hideIfOffline } = this.props;
  // if (hideIfOffline ... etc., etc.

and that would I think help a lot in separating the logic for the sake of reading and understanding this code.

Ultimately I'd want the substance of that method, especially the awayStatus.away ? 'offline' : statusFromPresence(...) part, to be in a selector or similar function somewhere, outside of display code. But I'm OK merging without that.

@borisyankov borisyankov force-pushed the presence-avatar branch 3 times, most recently from cabd599 to c76f7d2 Compare February 8, 2019 17:12
@borisyankov
Copy link
Contributor Author

Did the discussed changes. Ready for a merge?
A rename of the other selector is here #3326

@borisyankov borisyankov force-pushed the presence-avatar branch 2 times, most recently from 7ef7abe to 9734141 Compare February 8, 2019 21:01
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 @borisyankov for these updates!

Several of my comments seem to still be open, mainly on the first commit adding getUsersByEmail.

I think I can merge the refactor, though -- that will take a lot of the complexity out of the branch.

@@ -41,6 +41,13 @@ export const getAllUsersByEmail = createSelector(getAllUsers, allUsers =>
}, {}),
);

export const getUsersByEmail: Selector<UserEmailMap> = createSelector(getUsers, (users = []) =>
Copy link
Member

Choose a reason for hiding this comment

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

bump

@@ -41,6 +41,13 @@ export const getAllUsersByEmail = createSelector(getAllUsers, allUsers =>
}, {}),
);

export const getUsersByEmail: Selector<UserEmailMap> = createSelector(getUsers, (users = []) =>
users.reduce((usersById, user) => {
usersById[user.email] = user;
Copy link
Member

Choose a reason for hiding this comment

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

bump

users.reduce((usersById, user) => {
usersById[user.email] = user;
return usersById;
}, ({ ...undefined }: UserEmailMap)),
Copy link
Member

Choose a reason for hiding this comment

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

bump:

Also you mentioned in audio chat that you found this workaround in an issue thread on Flow. That link will be super useful to have written down.

return null;
}

const status = statusFromPresence(presence);
const status = statusFromPresenceAndUserStatus(userPresence, userStatus[user.user_id]);
Copy link
Member

Choose a reason for hiding this comment

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

Cool, this nicely encapsulates the business logic (such an odd term here; "product logic"?) at work here.

A further improvement would be to also stuff the next few lines above it, with user = usersByEmail[email] etc., inside. I think the ideal interface for that is probably to put this all inside a selector... e.g. the connect call could say

  presenceStatus: getPresenceStatusForEmail(state, props.email),

That's a pattern we don't currently use much, if at all, but there are a lot of places in our code that would benefit. Detailed discussion at #3015 .

I'm fine merging with this interface as it is, though.

@gnprice
Copy link
Member

gnprice commented Feb 8, 2019

Pushed the refactor and also the helper function, as 01b33ad..7faa1fe .

Now `connect`s to the `userStatus` state and if `away` for the
given user exists and is set to `true`, we override the calculated
value and make it `offline` instead.

It is unfortunate that there is a (temporary) discrepancy between
our `presence` and `userStatus` key used for mapping (`email` vs
`user_id`) thus needing the `usersByEmail` state too.

We should be moving `presence` to 'id to presence mapping' and then
we would replace the `email` prop with `userId` prop. Later.
@borisyankov
Copy link
Contributor Author

Rebased and now using the Map.

@gnprice gnprice merged commit db719ac into zulip:master Feb 12, 2019
@gnprice gnprice removed the review label Feb 12, 2019
@gnprice
Copy link
Member

gnprice commented Feb 12, 2019

Merged -- thanks again!

I'd still be glad to see the followup I mentioned in this comment:
#3326 (comment)

A further improvement would be to also stuff the next few lines above it, with user = usersByEmail[email] etc., inside.

@gnprice gnprice added the a-presence/status Presence, and "away status" label Mar 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-presence/status Presence, and "away status" P0 critical Highest priority
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants