Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
3.49k stars 2.85k forks source link

[HOLD for payment 2022-08-11 - Reminder to add bonus] Add skeleton UI to chats #7081

Closed mvtglobally closed 2 years ago

mvtglobally commented 2 years ago

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Action Performed:

  1. Open app
  2. Make sure you are on a slow connection (e.g. 3G)
  3. Open a new chat

Expected Result:

There should be some identifier that chat is loading

Actual Result:

Below image is the chat empty state when trying to load it on a bad connection. It'd be nice to put something in there while waiting

Workaround:

unknown

Platform:

Where is this issue occurring?

Version Number: 1.1.26-0 Reproducible in staging?: Y Reproducible in production?: Logs: https://stackoverflow.com/c/expensify/questions/4856 Notes/Photos/Videos: Any additional supporting documentation Image from iOS (9)

Image from iOS (10)

Expensify/Expensify Issue URL: Issue reported by: @puneetlath Slack conversation: https://expensify.slack.com/archives/C01GTK53T8Q/p1641514266065100

View all open jobs on GitHub

MelvinBot commented 2 years ago

Triggered auto assignment to @johncschuster (AutoAssignerTriage), see https://stackoverflow.com/c/expensify/questions/4749 for more details.

shawnborton commented 2 years ago

Following this one, I'd love to mock something up for this.

mananjadhav commented 2 years ago

Are we planning to add a shimmering effect (as shown in the 2nd screenshot) or some placeholders here?

johncschuster commented 2 years ago

Is this feature request still in the planning phase? If so, I'm not sure it would be appropriate to triage this to engineering right now.

puneetlath commented 2 years ago

@johncschuster it seems like we have agreement in the slack thread to go forward with it.

johncschuster commented 2 years ago

Sweet! In that case, I shall triage the thing. Thanks, @puneetlath!

MelvinBot commented 2 years ago

Triggered auto assignment to @madmax330 (Engineering), see https://stackoverflow.com/c/expensify/questions/4319 for more details.

madmax330 commented 2 years ago

@puneetlath have we decided on what the background should be? I.e do we already have the resources for it or does design need to come up with something first?

puneetlath commented 2 years ago

@shawnborton suggested going with a ghost UI like this in the slack thread.

image

@shawnborton do you feel good about going forward with that?

shawnborton commented 2 years ago

Yup! And then I like the idea of adding some kind of slight fading/pulsing effect to it.

madmax330 commented 2 years ago

Cool do we already have that asset or does that need to be created as part of this issue? (just trying to determine if it can be marked as external or not)

shawnborton commented 2 years ago

I suppose they are just shapes, so perhaps we don't need assets and we can just create them using containers and styles? Otherwise we can provide all of these as .svgs. Maybe it just depends on the proposals we get - I suppose there are multiple ways to implement this (either by creating containers or using svgs) and I'm curious which way you think is best.

mananjadhav commented 2 years ago

I have one or two options to implement, waiting for this to go "External" before applying.

madmax330 commented 2 years ago

I'm curious which way you think is best

I'm honestly not sure. But my guess would be svg, that way we're sure it'll render the same on all platforms. Not sure if making it with containers will have issue with different platforms. That's just a shot in the dark though. I guess we'll mark it as external and see what the proposals are

MelvinBot commented 2 years ago

Triggered auto assignment to @stephanieelliott (External), see https://stackoverflow.com/c/expensify/questions/8582 for more details.

stephanieelliott commented 2 years ago

Shared to Upwork: https://www.upwork.com/jobs/~01a3b83272750b939a

MelvinBot commented 2 years ago

Triggered auto assignment to Contributor-plus team member for initial proposal review - @parasharrajat (Exported)

MelvinBot commented 2 years ago

Triggered auto assignment to @Luke9389 (Exported), see https://stackoverflow.com/c/expensify/questions/7972 for more details.

parasharrajat commented 2 years ago

Yeah, I agree with Shawn. It can be done in many ways. Also, if we need to show the moving gradient effect over them then that needs to be animated. I am sure contributors will propose that as well. But can we please attach the image to issue body.

brianmuks commented 2 years ago

Proposal: I will use an Svg image for the skeleton then animate a gradient moving over the skeleton with react-native-reanimated. The skeleton (content-lazy-loading.svg) and the gradient (lazy-loader-gradient.svg) will be placed in :

App/assets/images/

then the component housing the logic will be placed in :

src/components/LazyLoader.js

The LazyLoader component will be rendered here: https://github.com/Expensify/App/blob/0bce09177d2aed8c5e377bc758f5241dd22886eb/src/pages/home/ReportScreen.js#L177 replacing <FullScreenLoadingIndicator visible={this.shouldShowLoader()} /> with

this.shouldShowLoader() &&  <LazyLoader />
mananjadhav commented 2 years ago

Are we guys looking to create a small component with SVGs? or a reusable component so that it can be like a library. I've got two ways to solve this.

Common Code in both approaches: In ReportScreen, we place FullScreenLoadingIndicator with GhostReportScreen component described later.

1. Single-Use Component with SVGs

2. Reusable lib without images

We create a GhostUI component that can be reused for any loader component. A sample (crude UI) looks like this:

https://user-images.githubusercontent.com/3069065/150652839-6f494572-2813-48f5-9ea2-3e7d7393b01c.mov

Its code flow then looks like:

 <View>
    <Animated.View
        style={{
            transform: [
                {
                    translateX: this.pulsingAnimation,
                },
            ],
            top: 0,
            bottom: 0,
            zIndex: 100,
            position: 'absolute',
            flexDirection: 'row',
        }}
    >
        {_.map(this.gradientOpacity, (opacity, i) => (
            <View
                key={i}
                style={{
                    transform: [
                        {
                            skewX: '-30deg',
                        },
                    ],
                    width: 20,
                    height: '100%',
                    backgroundColor: this.props.animationConfig.animationColor,
                    opacity,
                }}
            />
        ))}
    </Animated.View>
    {this.props.children}
</View>

I wasn't sure on best way to shorten the proposal, I hope it isn't tedious to review (a lot is related to Animated).

If we go ahead with point 1, then I am fine with a small budget, but if we take 2 I feel the budget should be increased as its going to be dynamic resuablle component and purely View, etc based with no additional dependencies.

parasharrajat commented 2 years ago

Thanks for the proposals guys.

  1. I don't think we will need the ghost loading for more views any soon. The main view is always going to be chat Body so investing in a generic component might not be worth it.

Using SVG Loader seems a good solution.

But we are missing one important step. How will you detect that chat is loading? this.shouldShowLoader is an illusion to give a sense of loading to match Native platforms. it does not really track the report loading.

brianmuks commented 2 years ago

But we are missing one important step. How will you detect that chat is loading? this.shouldShowLoader is an illusion to give a sense of loading to match Native platforms. it does not really track the report loading.

@parasharrajat How about using this.props.isLoadingReportActions from withOnyx ? First we initialize showSVGLoader to true

     this.state = {
            isLoading: true,
            showSVGLoader:true

        };

When this.props.isLoadingReportActions is false we set showSVGLoader to false


    componentDidUpdate(prevProps) {

     if(!this.props.isLoadingReportActions && this.state.showSVGLoader){
this.setState({showSVGLoader:false})
          }
.....
this.state.showSVGLoader &&  <LazyLoader />
export default withOnyx({
   isLoadingReportActions: {
        key: ONYXKEYS.IS_LOADING_REPORT_ACTIONS,
    },
....
parasharrajat commented 2 years ago

@brianmuks This key will be true many times when users will scroll up to load paginated messages. which will show the loader again and again.

  1. I think we only want to show the loader for the first time.

How will you tackle this issue?

brianmuks commented 2 years ago

@parasharrajat That is a good concern. kindly note that in the componentDidUpdate I am setting the local state (showSVGLoader) to false only once and it never goes back to true again. Meaning the lazyLoader won't be re-render when new users scroll up to load new messages.

   componentDidUpdate(prevProps) {

// I expect this to evaluate to true only once since I am never calling this.setState({showSVGLoader:true}) anywhere

     if(!this.props.isLoadingReportActions && this.state.showSVGLoader){
this.setState({showSVGLoader:false})
          }
parasharrajat commented 2 years ago

Oh, yeah. My bad completely overlooked it. But still, can we use ONYXKEYS.IS_LOADING_REPORT_DATA instead? I think when this is true all the initial data is loaded. Will it not work?

brianmuks commented 2 years ago

I can go with that.

parasharrajat commented 2 years ago

So you are saying that it will work. Have you tested it?

brianmuks commented 2 years ago

Yes I have. It works

parasharrajat commented 2 years ago

Ok, Sounds good. I didn't get a good sense of how you will implement the lazy loader. If you could explain briefly in technical words, that would be great.

brianmuks commented 2 years ago

@parasharrajat does this address your question or you need something more detailed. ?

Proposal: I will use an Svg image for the skeleton then animate a gradient moving over the skeleton with react-native-reanimated. The skeleton (content-lazy-loading.svg) and the gradient (lazy-loader-gradient.svg) will be placed in :

App/assets/images/

then the component housing the logic will be placed in :

src/components/LazyLoader.js

The LazyLoader component will be rendered here:

https://github.com/Expensify/App/blob/0bce09177d2aed8c5e377bc758f5241dd22886eb/src/pages/home/ReportScreen.js#L177

replacing <FullScreenLoadingIndicator visible={this.shouldShowLoader()} /> with

this.shouldShowLoader() &&  <LazyLoader />

Off-course keeping in mind the latest suggestions

parasharrajat commented 2 years ago

I will use an Svg image for the skeleton then animate a gradient moving over the skeleton with react-native-reanimated. The skeleton (content-lazy-loading.svg) and the gradient (lazy-loader-gradient.svg) will be placed in

I am asking to expand on this part.

brianmuks commented 2 years ago

I am asking to expand on this part.

So the whole idea is create small component that will render two SVG images. The first SVG image will be static one like the one below:

lazy loader

The second SVG image will be a gradient images that will move back and forth over the first SVG image using react-native-reanimated. The animation will be stopped when the component is unmounted:

gradient image walker
parasharrajat commented 2 years ago

Ok. Sounds good.

I like @brianmuks 's proposal. @mananjadhav has a great proposal too but @brianmuks came up first with the idea and @mananjadhav I see that you are already assigned to a couple of issues so this could be a good start for @brianmuks.

cc: @Luke9389

:ribbon: :eyes: :ribbon: C+ reviewed

Luke9389 commented 2 years ago

Is there a mockup for what this will look like on web/desktop? I wonder if we should have a different static svg for the wider screen sizes. @shawnborton did you happen to make a mockup for larger screens?

shawnborton commented 2 years ago

Actually wider screens is a great point. I think that strengthens the argument to not use an .svg for the actual placeholder shape, this way we can use a container for the fake text that can stretch as wide as we need it.

yUvLoSStOB

mananjadhav commented 2 years ago

I think that strengthens the argument to not use an .svg for the actual placeholder shape

@shawnborton @Luke9389 @parasharrajat Wrt to the comment, do you want to explore solution 2 in this comment. https://github.com/Expensify/App/issues/7081#issuecomment-1019341453

The (not so good-looking) video I've attached with the proposal is pure View and Text to load the view.

shawnborton commented 2 years ago

That's what I am thinking, yeah.

parasharrajat commented 2 years ago

BUt SVG can expand as much as we want. They fill in the available space.

shawnborton commented 2 years ago

Fair point, but I suppose for that to work, we'd need individual svgs for each element of the placeholder seeing as we'd only stretch some and not all. That's totally fine with me, but at that point, why would that be better than just drawing shapes as views?

mananjadhav commented 2 years ago

Are we okay to explore adding a dependency? https://www.npmjs.com/package/react-content-loader Makes it reusable for future use cases and very small size footprint.

parasharrajat commented 2 years ago

No extra processing. SVG runs on the UI layer.

But I would say, lets do whatever is easier.

Luke9389 commented 2 years ago

After reading the code for this package, https://www.npmjs.com/package/react-content-loader, I would be OK with using it. It uses another package called 'react-native-svg' to handle svgs in the native environment, and uses an <svg> tag for web.

I'm going to open this up for a few more opinions before moving forward. See you all on slack!

Luke9389 commented 2 years ago

Here's the slack convo I mentioned above: https://expensify.slack.com/archives/C01GTK53T8Q/p1643743784049659

We're OK with using react-content-loader 👍

Luke9389 commented 2 years ago

@parasharrajat, how would you like to do this? react-content-loader was @mananjadhav's idea. @mananjadhav are you still busy with other issues?

I think we still want a full proposal either way, that talks about using react-content-loader. Do you agree @parasharrajat ?

parasharrajat commented 2 years ago

Yeah, package looks good to me it uses SVG which is cool.

Let's give this opportunity to @mananjadhav . @mananjadhav could you please post a new proposal explaining bits and pieces as now it's clear on what should be done?

mananjadhav commented 2 years ago

I'll send in a proposal in a few hours.

mananjadhav commented 2 years ago

Revised Proposal

  1. We add react-content-loader as a dependency
  2. We create a component ChatGhostUI
  3. It'll have two files -> index.js and index.native.js
  4. A simple snippet for the code would look like this:
import React from 'react';
import ContentLoader from 'react-content-loader';
import {Dimensions} from 'react-native';

const ChatList = (props) => {
    const windowWidth = Dimensions.get('window').width;
    const windowHeight = Dimensions.get('window').height;

    return (
        <ContentLoader viewBox={`0 0 ${windowWidth} ${windowHeight}`} height="100%" width="100%" {...props}>
            <circle cx="70.2" cy="73.2" r="41.3" />
            <rect x="129.9" y="29.5" width="80%" height="17" />
            <rect x="129.9" y="64.7" width="296" height="17" />
            <rect x="129.9" y="97.8" width="253.5" height="17" />
            <rect x="129.9" y="132.3" width="212.5" height="17" />

            <circle cx="70.7" cy="243.5" r="41.3" />
            <rect x="130.4" y="199.9" width="125.5" height="17" />
            <rect x="130.4" y="235" width="296" height="17" />
            <rect x="130.4" y="268.2" width="253.5" height="17" />
            <rect x="130.4" y="302.6" width="212.5" height="17" />

            <circle cx="70.7" cy="412.7" r="41.3" />
            <rect x="130.4" y="369" width="125.5" height="17" />
            <rect x="130.4" y="404.2" width="296" height="17" />
            <rect x="130.4" y="437.3" width="253.5" height="17" />
            <rect x="130.4" y="471.8" width="212.5" height="17" />
        </ContentLoader>
    );
};
export default ChatList;
  1. For index.js, we'll have to import import ContentLoader, { Rect, Circle } from 'react-content-loader/native'

Here's what it looks like:

https://user-images.githubusercontent.com/3069065/152167749-090fbc67-0706-4c09-9900-b7e1d01ab7a2.mov

parasharrajat commented 2 years ago

Didn't understand this part?

For index.js, we'll have to import import ContentLoader, { Rect, Circle } from 'react-content-loader/native'

How will you make sure that the UI screen is completely filled with loader irrespective of the height of the screen?

mananjadhav commented 2 years ago

How will you make sure that the UI screen is completely filled with loader irrespective of the height of the screen?

Here's the working code.

import React from 'react';
import {Dimensions} from 'react-native';
import ContentLoader from 'react-content-loader';
import _ from 'underscore';

const ChatList = (props) => {
    const windowHeight = Dimensions.get('window').height;

    const height = 140;
    const numberOfRows = Math.floor(windowHeight / height);
    const contentItems = Array.from({length: numberOfRows}, (v, i) => i);

    return (
        <>
            {
                _.map(contentItems, item => (
                    <ContentLoader height={height} width="100%" key={item} {...props}>
                        <circle cx="50" cy="70" r="30" />
                        <rect x="100" y="64.7" width="30%" height="17" />
                        <rect x="100" y="29.5" width="85%" height="17" />
                        <rect x="100" y="97.8" width="60%" height="16" />
                    </ContentLoader>
                ))
            }
        </>
    );
};
export default ChatList;

What I'll need help with is, will the size of the placeholder be the same across devices or will change for small or medium devices? Accordingly, I can set sizes for the svgs

Didn't understand this part?

For index.js, we'll have to import import ContentLoader, { Rect, Circle } from 'react-content-loader/native'

I meant index.native.js. Sorry typo. Instead of using circ and rect, we'll import Circle and Rect for native platforms.