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

TBD Proposal cards animation #4996

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-Nns-Dapp-unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ proposal is successful, the changes it released will be moved from this file to
#### Added

* Enabled `ENABLE_CKUSDC` feature flag.
* Proposal cards animation.

#### Changed

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/lib/components/launchpad/Proposals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SkeletonProposalCard from "$lib/components/ui/SkeletonProposalCard.svelte";
import NnsProposalCard from "../proposals/NnsProposalCard.svelte";
import { loadProposalsSnsCF } from "$lib/services/$public/sns.services";
import { fade } from "svelte/transition";

let loading = false;
$: loading = $snsProposalsStoreIsLoading;
Expand All @@ -22,16 +23,18 @@
</script>

{#if loading}
<div class="card-grid">
<div in:fade class="card-grid">
<SkeletonProposalCard />
<SkeletonProposalCard />
</div>
{:else if $openSnsProposalsStore.length === 0}
<p class="no-proposals description">{$i18n.sns_launchpad.no_proposals}</p>
<p in:fade class="no-proposals description">
{$i18n.sns_launchpad.no_proposals}
</p>
{:else}
<ul class="card-grid">
{#each $openSnsProposalsStore as proposalInfo (proposalInfo.id)}
<NnsProposalCard {proposalInfo} />
{#each $openSnsProposalsStore as proposalInfo, index (proposalInfo.id)}
<NnsProposalCard {proposalInfo} {index} />
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really want the proposal on the SNS page to slide in while the other 2 cards don't?

{/each}
</ul>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { getProjectProposal } from "$lib/getters/sns-summary";
import { loadProposal } from "$lib/services/$public/proposals.services";
import { i18n } from "$lib/stores/i18n";
import { fade } from "svelte/transition";

export let summary: SnsSummary;

Expand All @@ -31,7 +32,7 @@
</script>

{#if nonNullish(proposalInfo)}
<h3>{$i18n.sns_project_detail.swap_proposal}</h3>
<h3 in:fade>{$i18n.sns_project_detail.swap_proposal}</h3>
<NnsProposalCard {proposalInfo} />
{/if}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

<TestIdWrapper testId="actionable-nns-proposals-component">
{#if $actionableNnsProposalsStore?.proposals?.length ?? 0 > 0}
<UniverseWithActionableProposals universe={$nnsUniverseStore}>
{#each $actionableNnsProposalsStore?.proposals ?? [] as proposalInfo (proposalInfo.id)}
<NnsProposalCard actionable fromActionablePage {proposalInfo} />
<UniverseWithActionableProposals universe={$nnsUniverseStore} noAnimation>
{#each $actionableNnsProposalsStore?.proposals ?? [] as proposalInfo, index (proposalInfo.id)}
<NnsProposalCard actionable fromActionablePage {proposalInfo} {index} />
{/each}
</UniverseWithActionableProposals>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import { Principal } from "@dfinity/principal";
import type { Universe } from "$lib/types/universe";
import type { Readable } from "svelte/store";
import { PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND } from "$lib/constants/constants";

export let universe: Universe;
export let proposals: ProposalData[];
export let cardIndex = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

How about cardIndexOffset?


let rootCanisterId: RootCanisterIdText;
$: rootCanisterId = universe.canisterId;
Expand All @@ -27,9 +29,13 @@

<TestIdWrapper testId="actionable-sns-proposals-component">
{#if nonNullish(nsFunctions)}
<UniverseWithActionableProposals {universe}>
{#each proposals as proposalData (fromNullable(proposalData.id)?.id)}
<UniverseWithActionableProposals
{universe}
delay={cardIndex * PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND}
>
{#each proposals as proposalData, index (fromNullable(proposalData.id)?.id)}
<SnsProposalCard
index={cardIndex + index}
actionable
fromActionablePage
{proposalData}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<script lang="ts">
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import {
type ActionableSnsProposalsByUniverseData,
actionableSnsProposalsByUniverseStore,
} from "$lib/derived/actionable-proposals.derived";
import ActionableSnsProposals from "$lib/components/proposals/ActionableSnsProposals.svelte";

export let cardIndex = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

cardIndexOffset?


let actionableUniverses: ActionableSnsProposalsByUniverseData[] = [];
$: actionableUniverses = $actionableSnsProposalsByUniverseStore.filter(
({ proposals }) => proposals.length > 0
);
</script>

<TestIdWrapper testId="actionable-snses-component">
<div data-tid="actionable-snses-component">
{#each actionableUniverses as { universe, proposals } (universe.canisterId)}
<ActionableSnsProposals {universe} {proposals} />
<ActionableSnsProposals {universe} {proposals} {cardIndex} />
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't cardIndex be incremented per each universe by the number of cards before it?

{/each}
</TestIdWrapper>
</div>
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script>
import SkeletonCard from "../ui/SkeletonCard.svelte";
import { fade } from "svelte/transition";
</script>

<div class="card-grid" data-tid="proposals-loading">
<div class="card-grid" data-tid="proposals-loading" in:fade|global>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
export let hidden = false;
export let actionable = false;
export let fromActionablePage = false;
export let index = 0;

let id: ProposalId | undefined;
let title: string | undefined;
Expand All @@ -35,6 +36,7 @@

<ProposalCard
{hidden}
{index}
{actionable}
{href}
{status}
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/lib/components/proposals/NnsProposalsList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import { building } from "$app/environment";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import { actionableNnsProposalsStore } from "$lib/stores/actionable-nns-proposals.store";
import { fade } from "svelte/transition";
import type { ProposalInfo } from "@dfinity/nns";
import { isNullish } from "@dfinity/utils";
import ActionableProposalsSignIn from "$lib/components/proposals/ActionableProposalsSignIn.svelte";
Expand All @@ -36,7 +35,7 @@

{#if display}
{#if !$actionableProposalsActiveStore}
<div in:fade data-tid="all-proposal-list">
<div data-tid="all-proposal-list">
{#if loadingAnimation === "skeleton"}
<LoadingProposals />
{:else if nothingFound}
Expand All @@ -48,9 +47,10 @@
layout="grid"
disabled={disableInfiniteScroll || loading}
>
{#each $filteredActionableProposals.proposals as proposalInfo (proposalInfo.id)}
{#each $filteredActionableProposals.proposals as proposalInfo, index (proposalInfo.id)}
<NnsProposalCard
{hidden}
{index}
actionable={proposalInfo.isActionable}
{proposalInfo}
/>
Expand All @@ -60,7 +60,7 @@
{/if}
</div>
{:else}
<div in:fade data-tid="actionable-proposal-list">
<div data-tid="actionable-proposal-list">
{#if !$authSignedInStore}
<ActionableProposalsSignIn />
{:else if isNullish(actionableProposals)}
Expand All @@ -69,8 +69,8 @@
<ActionableProposalsEmpty />
{:else}
<InfiniteScroll layout="grid" disabled>
{#each actionableProposals ?? [] as proposalInfo (proposalInfo.id)}
<NnsProposalCard {hidden} actionable {proposalInfo} />
{#each actionableProposals ?? [] as proposalInfo, index (proposalInfo.id)}
<NnsProposalCard {hidden} {index} actionable {proposalInfo} />
{/each}
</InfiniteScroll>
{/if}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/components/proposals/NoProposals.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { i18n } from "$lib/stores/i18n";
import { fade } from "svelte/transition";
</script>

<p class="description" data-tid="no-proposals-msg">
<p class="description" data-tid="no-proposals-msg" in:fade|global>
{$i18n.voting.nothing_found}
</p>
49 changes: 46 additions & 3 deletions frontend/src/lib/components/proposals/ProposalCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@
import { i18n } from "$lib/stores/i18n";
import Countdown from "./Countdown.svelte";
import { nowInSeconds } from "$lib/utils/date.utils";
import { nonNullish } from "@dfinity/utils";
import { isNullish, nonNullish } from "@dfinity/utils";
import { shortenWithMiddleEllipsis } from "$lib/utils/format.utils";
import { PROPOSER_ID_DISPLAY_SPLIT_LENGTH } from "$lib/constants/proposals.constants";
import type { UniversalProposalStatus } from "$lib/types/proposals";
import ProposalStatusTag from "$lib/components/ui/ProposalStatusTag.svelte";

import { fly, type FlyParams } from "svelte/transition";
import {
PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND,
PROPOSAL_CARD_ANIMATION_DURATION_IN_MILLISECOND,
PROPOSAL_CARD_ANIMATION_EASING,
PROPOSAL_CARD_ANIMATION_Y,
} from "$lib/constants/constants";
import { isNode } from "$lib/utils/dev.utils";
import { isHtmlElementInViewport } from "$lib/utils/utils";

export let index = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use a more specific name? Something like animationIndex?
Same for similar props in other card components.

export let hidden = false;
export let actionable = false;
export let status: UniversalProposalStatus | undefined;
Expand All @@ -25,9 +35,41 @@
export let proposer: string | undefined;
export let deadlineTimestampSeconds: bigint | undefined;
export let href: string;

let element: HTMLElement;

let inViewport: boolean = false;
$: inViewport =
isNode() || isNullish(element) ? true : isHtmlElementInViewport(element);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why exactly do we call isNode here from dev.utils?

Copy link
Contributor

Choose a reason for hiding this comment

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

If I read the logic correctly, we treat an element that is null is being in the viewport. Is that correct?


// A short delay to wait when the cards are being rendered.
// This process takes some time on FF and Safari, which makes the animation look not perfect.
const CARD_ANIMATION_DELAY = 250;
// Apply sequential fly animation to only first cards:
// 1. to not waist resources on not visible animation;
// 2. to make the proposal lazy loading still looks good.
const TOP_MAX_ANIMATED_CARDS = 18;
let topCards = false;
$: topCards = index < TOP_MAX_ANIMATED_CARDS;
let flyAnimation: FlyParams = {};
$: flyAnimation = {
duration: topCards ? PROPOSAL_CARD_ANIMATION_DURATION_IN_MILLISECOND : 0,
delay: topCards
? index * PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND +
CARD_ANIMATION_DELAY
: // Adjust the animation start time for cards other than the first ones,
// so they appear right after the first cards becomes visible.
TOP_MAX_ANIMATED_CARDS * PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND +
CARD_ANIMATION_DELAY +
PROPOSAL_CARD_ANIMATION_DURATION_IN_MILLISECOND / 2,
// Do not apply any animation to the cards that are not in the viewport.
y: inViewport ? PROPOSAL_CARD_ANIMATION_Y : 0,
opacity: inViewport ? 0 : 1,
easing: PROPOSAL_CARD_ANIMATION_EASING,
};
</script>

<li class:hidden>
<li bind:this={element} class:hidden in:fly|global={flyAnimation}>
<Card testId="proposal-card" {href}>
<div class="container">
<div>
Expand Down Expand Up @@ -95,6 +137,7 @@

li {
list-style: none;
will-change: transform, opacity;
}

.container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@
import type { Universe } from "$lib/types/universe";
import { InfiniteScroll } from "@dfinity/gix-components";
import UniversePageSummary from "$lib/components/universe/UniversePageSummary.svelte";
import { fade } from "svelte/transition";
import { SVELTE_DEFAULT_ANIMATION_DURATION_IN_MILLISECOND } from "$lib/constants/constants";

export let universe: Universe;
export let delay = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use a clearer name. Something like titleFadeInDelay?

export let noAnimation = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this prop? Just leaving the delay at 0 has the same effect, no?

</script>

<div class="container" data-tid="universe-with-actionable-proposals-component">
<div class="title">
<div
in:fade|global={{
delay,
duration: noAnimation
? 0
: SVELTE_DEFAULT_ANIMATION_DURATION_IN_MILLISECOND,
}}
class="title"
>
<UniversePageSummary {universe} />
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
export let actionable = false;
export let fromActionablePage = false;
export let hidden = false;
export let index = 0;

let id: SnsProposalId | undefined;
let title: string | undefined;
Expand Down Expand Up @@ -53,6 +54,7 @@
<ProposalCard
{status}
{hidden}
{index}
{actionable}
{href}
id={id?.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<SnsProposalsFilters />

{#if !actionableSelected}
<div in:fade data-tid="all-proposal-list">
<div data-tid="all-proposal-list">
{#if proposals === undefined}
<LoadingProposals />
{:else if proposals.length === 0}
Expand All @@ -41,8 +41,9 @@
on:nnsIntersect
disabled={disableInfiniteScroll}
>
{#each proposals as proposalData (fromNullable(proposalData.id)?.id)}
{#each proposals as proposalData, index (fromNullable(proposalData.id)?.id)}
<SnsProposalCard
{index}
actionable={proposalData.isActionable}
{proposalData}
{nsFunctions}
Expand All @@ -65,8 +66,9 @@
<ActionableProposalsEmpty />
{:else}
<InfiniteScroll layout="grid" disabled>
{#each proposals as proposalData (fromNullable(proposalData.id)?.id)}
{#each proposals as proposalData, index (fromNullable(proposalData.id)?.id)}
<SnsProposalCard
{index}
actionable={proposalData.isActionable}
{proposalData}
rootCanisterId={$pageStore.universe}
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/lib/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { cubicOut } from "svelte/easing";

export const DEFAULT_LIST_PAGINATION_LIMIT = 100;
export const MAX_ACTIONABLE_REQUEST_COUNT = 5;
// Use a different limit for Icrc transactions
Expand All @@ -22,3 +24,12 @@ export const DAYS_IN_NON_LEAP_YEAR = 365;

export const NANO_SECONDS_IN_MILLISECOND = 1_000_000;
export const NANO_SECONDS_IN_MINUTE = NANO_SECONDS_IN_MILLISECOND * 1_000 * 60;

// Animation constants
export const SVELTE_DEFAULT_ANIMATION_DURATION_IN_MILLISECOND = 400;
export const PROPOSAL_CARD_ANIMATION_DURATION_IN_MILLISECOND =
SVELTE_DEFAULT_ANIMATION_DURATION_IN_MILLISECOND;
// Maximum 10 card animations at a time
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this comment refer to?
It doesn't seem to have anything to do with the line above or below it.

And should it be 18 instead of 10? The PR description says 18. Or is that something else?

export const PROPOSAL_CARD_ANIMATION_DELAY_IN_MILLISECOND = 40;
export const PROPOSAL_CARD_ANIMATION_Y = 50;
export const PROPOSAL_CARD_ANIMATION_EASING = cubicOut;
Loading
Loading