ChannelsCards.tsx raw
1 import { InfoIcon } from "lucide-react";
2 import { ChannelDropdownMenu } from "src/components/channels/ChannelDropdownMenu";
3 import { ChannelWarning } from "src/components/channels/ChannelWarning";
4 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
5 import { Badge } from "src/components/ui/badge.tsx";
6 import {
7 Card,
8 CardContent,
9 CardDescription,
10 CardHeader,
11 CardTitle,
12 } from "src/components/ui/card";
13 import { Progress } from "src/components/ui/progress.tsx";
14 import { Separator } from "src/components/ui/separator";
15 import {
16 Tooltip,
17 TooltipContent,
18 TooltipProvider,
19 TooltipTrigger,
20 } from "src/components/ui/tooltip";
21 import { useNodeDetails } from "src/hooks/useNodeDetails";
22 import { formatAmount } from "src/lib/utils.ts";
23 import { Channel, LongUnconfirmedZeroConfChannel } from "src/types";
24
25 type ChannelsCardsProps = {
26 channels?: Channel[];
27 longUnconfirmedZeroConfChannels: LongUnconfirmedZeroConfChannel[];
28 };
29
30 export function ChannelsCards({
31 channels,
32 longUnconfirmedZeroConfChannels,
33 }: ChannelsCardsProps) {
34 if (!channels?.length) {
35 return null;
36 }
37
38 return (
39 <Card className="lg:hidden">
40 <CardHeader className="w-full pb-2 text-2xl font-semibold">
41 Channels
42 </CardHeader>
43 <CardContent>
44 {channels
45 .sort((a, b) =>
46 a.localBalanceMsat + a.remoteBalanceMsat >
47 b.localBalanceMsat + b.remoteBalanceMsat
48 ? -1
49 : 1
50 )
51 .map((channel, index) => {
52 const unconfirmedChannel = longUnconfirmedZeroConfChannels.find(
53 (uc) => uc.id === channel.id
54 );
55 return (
56 <ChannelCard
57 key={index}
58 addSeparator={index > 0}
59 channel={channel}
60 unconfirmedChannel={unconfirmedChannel}
61 hasMultipleChannels={channels.length > 1}
62 />
63 );
64 })}
65 </CardContent>
66 </Card>
67 );
68 }
69 type ChannelCardProps = {
70 channel: Channel;
71 unconfirmedChannel: LongUnconfirmedZeroConfChannel | undefined;
72 addSeparator: boolean;
73 hasMultipleChannels: boolean;
74 };
75
76 function ChannelCard({
77 channel,
78 unconfirmedChannel,
79 addSeparator,
80 hasMultipleChannels,
81 }: ChannelCardProps) {
82 const { data: node } = useNodeDetails(channel.remotePubkey);
83 const alias = node?.alias || "Unknown";
84 const capacityMsat = channel.localBalanceMsat + channel.remoteBalanceMsat;
85
86 return (
87 <>
88 {addSeparator && <Separator className="mt-6 -mb-2" />}
89 <div className="flex flex-col items-start w-full">
90 <CardHeader className="pb-4 px-0 w-full">
91 <CardTitle className="w-full">
92 <div className="flex items-center justify-between">
93 <div className="flex-1 whitespace-nowrap text-ellipsis font-semibold truncate leading-normal">
94 {alias}
95 </div>
96 <ChannelDropdownMenu
97 alias={alias}
98 channel={channel}
99 hasMultipleChannels={hasMultipleChannels}
100 />
101 </div>
102 </CardTitle>
103 </CardHeader>
104 <CardDescription className="w-full flex flex-col gap-4">
105 <div className="flex w-full justify-between items-center">
106 <p className="text-muted-foreground font-medium">Status</p>
107 {channel.status == "online" ? (
108 unconfirmedChannel ? (
109 <Badge variant="outline" title={unconfirmedChannel.message}>
110 Unconfirmed
111 </Badge>
112 ) : (
113 <Badge variant="positive">Online</Badge>
114 )
115 ) : channel.status == "opening" ? (
116 <Badge variant="outline">Opening</Badge>
117 ) : (
118 <Badge variant="warning">Offline</Badge>
119 )}
120 </div>
121 <div className="flex w-full justify-between items-center">
122 <TooltipProvider>
123 <Tooltip>
124 <TooltipTrigger>
125 <div className="flex flex-row gap-1 items-center text-muted-foreground">
126 Type
127 <InfoIcon className="h-3 w-3 shrink-0" />
128 </div>
129 </TooltipTrigger>
130 <TooltipContent>
131 The type of lightning channel, By default private channel is
132 recommended. If you a podcaster or musician and expect to
133 receive keysend or Value4Value payments you will need a public
134 channel.
135 </TooltipContent>
136 </Tooltip>
137 </TooltipProvider>
138 <p className="text-foreground">
139 {channel.public ? "Public" : "Private"}
140 </p>
141 </div>
142 <div className="flex justify-between items-center">
143 <TooltipProvider>
144 <Tooltip>
145 <TooltipTrigger>
146 <div className="flex flex-row gap-1 items-center text-muted-foreground">
147 Capacity
148 <InfoIcon className="h-3 w-3 shrink-0" />
149 </div>
150 </TooltipTrigger>
151 <TooltipContent>
152 Total Spending and Receiving capacity of your lightning
153 channel.
154 </TooltipContent>
155 </Tooltip>
156 </TooltipProvider>
157
158 <p className="text-foreground">
159 <FormattedBitcoinAmount amountMsat={capacityMsat} />
160 </p>
161 </div>
162 <div className="flex justify-between items-center">
163 <TooltipProvider>
164 <Tooltip>
165 <TooltipTrigger>
166 <div className="flex flex-row gap-1 items-center text-muted-foreground">
167 Reserve
168 <InfoIcon className="h-3 w-3 shrink-0" />
169 </div>
170 </TooltipTrigger>
171 <TooltipContent>
172 Funds each participant sets aside to discourage cheating by
173 ensuring each party has something at stake. This reserve
174 cannot be spent during the channel's lifetime and typically
175 amounts to 1% of the channel capacity. The reserve will
176 automatically be filled when payments are received on the
177 channel.
178 </TooltipContent>
179 </Tooltip>
180 </TooltipProvider>
181
182 <p className="text-foreground">
183 {channel.localBalanceMsat <
184 channel.unspendablePunishmentReserveSat * 1000 && (
185 <>
186 {formatAmount(
187 Math.min(
188 channel.localBalanceMsat,
189 channel.unspendablePunishmentReserveSat * 1000
190 )
191 )}{" "}
192 /{" "}
193 </>
194 )}
195 <FormattedBitcoinAmount
196 amountMsat={channel.unspendablePunishmentReserveSat * 1000}
197 />
198 </p>
199 </div>
200 <div className="flex justify-between items-center">
201 <p className="text-muted-foreground font-medium text-sm">
202 Spending
203 </p>
204 <p className="text-muted-foreground font-medium text-sm">
205 Receiving
206 </p>
207 </div>
208 <div className="flex gap-2 items-center">
209 <div className="flex-1 relative">
210 <Progress
211 value={(channel.localSpendableBalanceMsat / capacityMsat) * 100}
212 className="h-6 absolute"
213 />
214 <div className="flex flex-row w-full justify-between px-2 text-xs items-center h-6 mix-blend-exclusion text-white">
215 <span>
216 <FormattedBitcoinAmount
217 amountMsat={channel.localSpendableBalanceMsat}
218 />
219 </span>
220 <span>
221 <FormattedBitcoinAmount
222 amountMsat={channel.remoteBalanceMsat}
223 />
224 </span>
225 </div>
226 </div>
227 <ChannelWarning channel={channel} />
228 </div>
229 </CardDescription>
230 </div>
231 </>
232 );
233 }
234