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

feat: add pagination to tx list #895

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,11 @@ type BalancesResponse = lnclient.BalancesResponse
type SendPaymentResponse = Transaction
type MakeInvoiceResponse = Transaction
type LookupInvoiceResponse = Transaction
type ListTransactionsResponse = []Transaction

type ListTransactionsResponse struct {
TotalCount int64 `json:"totalCount"`
Transactions []Transaction `json:"transactions"`
}

// TODO: camelCase
type Transaction struct {
Expand Down
8 changes: 5 additions & 3 deletions api/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupI
return toApiTransaction(transaction), nil
}

// TODO: accept offset, limit params for pagination
func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transactions, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), appId, true)
transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), appId, true)
if err != nil {
return nil, err
}
Expand All @@ -50,7 +49,10 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64,
apiTransactions = append(apiTransactions, *toApiTransaction(&transaction))
}

return &apiTransactions, nil
return &ListTransactionsResponse{
Transactions: apiTransactions,
TotalCount: totalCount,
}, nil
}

func (api *api) SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"prepare": "cd .. && husky frontend/.husky"
},
"dependencies": {
"@getalby/lightning-tools": "^5.1.1",
"@getalby/bitcoin-connect-react": "^3.6.2",
"@getalby/lightning-tools": "^5.1.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
Expand All @@ -36,7 +36,7 @@
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
Expand Down
97 changes: 92 additions & 5 deletions frontend/src/components/TransactionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { Drum } from "lucide-react";
import { useEffect, useState } from "react";
import EmptyState from "src/components/EmptyState";
import Loading from "src/components/Loading";
import TransactionItem from "src/components/TransactionItem";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "src/components/ui/pagination";
import { LIST_TRANSACTIONS_LIMIT } from "src/constants";
import { useTransactions } from "src/hooks/useTransactions";
import { generatePageNumbers } from "src/lib/utils";

type TransactionsListProps = {
appId?: number;
Expand All @@ -13,15 +25,39 @@ function TransactionsList({
appId,
showReceiveButton = true,
}: TransactionsListProps) {
const { data: transactions, isLoading } = useTransactions(appId);
const [page, setPage] = useState(1);
const { data: transactionData, isLoading } = useTransactions(
appId,
false,
LIST_TRANSACTIONS_LIMIT,
page
);

useEffect(() => {
const el = document.querySelector(".transaction-list");
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, [page]);

if (isLoading) {
if (isLoading || !transactionData) {
return <Loading />;
}

const transactions = transactionData?.transactions || [];
const totalCount = transactionData?.totalCount || 0;
const totalPages = Math.ceil(totalCount / LIST_TRANSACTIONS_LIMIT);

const handlePageChange = (newPage: number) => {
if (newPage < 1 || newPage > totalPages) {
return;
}
setPage(newPage);
};

return (
<div className="transaction-list flex flex-col">
{!transactions?.length ? (
{!transactions.length ? (
<EmptyState
icon={Drum}
title="No transactions yet"
Expand All @@ -32,9 +68,60 @@ function TransactionsList({
/>
) : (
<>
{transactions?.map((tx) => {
return <TransactionItem key={tx.paymentHash + tx.type} tx={tx} />;
{transactions?.map((tx, i) => {
return (
<TransactionItem key={tx.paymentHash + tx.type + i} tx={tx} />
);
})}

{totalPages > 1 && (
<div className="mt-4 self-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
handlePageChange(page - 1);
}}
/>
</PaginationItem>

{generatePageNumbers(page, totalPages).map((p, index) =>
p === "ellipsis" ? (
<PaginationItem key={index}>
<PaginationEllipsis className="flex items-center" />
</PaginationItem>
) : (
<PaginationItem key={p}>
<PaginationLink
href="#"
isActive={p === page}
onClick={(e) => {
e.preventDefault();
handlePageChange(p);
}}
>
{p}
</PaginationLink>
</PaginationItem>
)
)}

<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
handlePageChange(page + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</>
)}
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/layouts/SendLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export default function SendLayout() {
const { hasChannelManagement } = useInfo();
const { data: balances } = useBalances();
const { data: channels } = useChannels();
const { data: transactions } = useTransactions();
const { data: transactionData } = useTransactions();

if (!balances || !channels) {
if (!balances || !channels || !transactionData) {
return <Loading />;
}

Expand All @@ -28,7 +28,7 @@ export default function SendLayout() {
title="Send"
description="Pay a lightning invoice created by any bitcoin lightning wallet"
/>
{transactions?.some(
{transactionData.transactions?.some(
(tx) =>
tx.state === "pending" &&
dayjs().diff(dayjs(tx.createdAt)) <
Expand Down
120 changes: 120 additions & 0 deletions frontend/src/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from "react";
import { cn } from "src/lib/utils";
import { ButtonProps, buttonVariants } from "src/components/ui/button";
import {
ChevronLeftIcon,
ChevronRightIcon,
DotsHorizontalIcon,
} from "@radix-ui/react-icons";

const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";

const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
));
PaginationContent.displayName = "PaginationContent";

const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";

type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;

const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";

const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeftIcon className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";

const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";

const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";

export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};
2 changes: 2 additions & 0 deletions frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const localStorageKeys = {
export const ONCHAIN_DUST_SATS = 1000;
export const ALBY_HIDE_HOSTED_BALANCE_BELOW = 100;
export const ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL = 30_000;

export const LIST_TRANSACTIONS_LIMIT = 10;
16 changes: 10 additions & 6 deletions frontend/src/hooks/useNotifyReceivedPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { useTransactions } from "src/hooks/useTransactions";
import { Transaction } from "src/types";

export function useNotifyReceivedPayments() {
const { data: transactions } = useTransactions(undefined, true, 1);
const { data } = useTransactions(undefined, true, 1);
const [prevTransaction, setPrevTransaction] = React.useState<Transaction>();
const { toast } = useToast();

React.useEffect(() => {
if (transactions && prevTransaction !== transactions[0]) {
if (prevTransaction && transactions[0].type === "incoming") {
if (!data?.transactions?.length) {
return;
}
const latestTx = data.transactions[0];
if (latestTx !== prevTransaction) {
if (prevTransaction && latestTx.type === "incoming") {
toast({
title: "Payment received",
description: `${new Intl.NumberFormat().format(Math.floor(transactions[0].amount / 1000))} sats`,
description: `${new Intl.NumberFormat().format(Math.floor(latestTx.amount / 1000))} sats`,
});
}
setPrevTransaction(transactions[0]);
setPrevTransaction(latestTx);
}
}, [prevTransaction, toast, transactions]);
}, [prevTransaction, toast, data]);
}
6 changes: 3 additions & 3 deletions frontend/src/hooks/useOnboardingData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
const { data: channels } = useChannels();
const { data: info, hasChannelManagement, hasMnemonic } = useInfo();
const { data: nodeConnectionInfo } = useNodeConnectionInfo();
const { data: transactions } = useTransactions(undefined, false, 1);
const { data: transactionData } = useTransactions(undefined, false, 1);

const isLoading =
!apps ||
!channels ||
!info ||
!nodeConnectionInfo ||
!transactions ||
!transactionData ||
(info.albyAccountConnected && (!albyMe || !albyBalance));

if (isLoading) {
Expand All @@ -55,7 +55,7 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
new Date(info.nextBackupReminder).getTime() > new Date().getTime();
const hasCustomApp =
apps && apps.find((x) => x.name !== "getalby.com") !== undefined;
const hasTransaction = transactions.length > 0;
const hasTransaction = transactionData.totalCount > 0;

const checklistItems: Omit<ChecklistItem, "disabled">[] = [
{
Expand Down
Loading
Loading