diff --git a/src/app/home/index.tsx b/src/app/home/index.tsx
index c181a7d7..37a39167 100644
--- a/src/app/home/index.tsx
+++ b/src/app/home/index.tsx
@@ -20,7 +20,7 @@ import {
WidgetList,
} from '@shared/widgets';
import { WIDGET_KIND } from '@utils/constants';
-import { Widget } from '@utils/types';
+import { WidgetProps } from '@utils/types';
export function HomeScreen() {
const ark = useArk();
@@ -54,32 +54,32 @@ export function HomeScreen() {
staleTime: Infinity,
});
- const renderItem = (widget: Widget) => {
+ const renderItem = (widget: WidgetProps) => {
switch (widget.kind) {
case WIDGET_KIND.notification:
return ;
case WIDGET_KIND.newsfeed:
return ;
case WIDGET_KIND.topic:
- return ;
+ return ;
case WIDGET_KIND.user:
- return ;
+ return ;
case WIDGET_KIND.thread:
- return ;
+ return ;
case WIDGET_KIND.article:
- return ;
+ return ;
case WIDGET_KIND.file:
- return ;
+ return ;
case WIDGET_KIND.hashtag:
- return ;
+ return ;
case WIDGET_KIND.group:
- return ;
+ return ;
case WIDGET_KIND.trendingNotes:
- return ;
+ return ;
case WIDGET_KIND.trendingAccounts:
- return ;
+ return ;
case WIDGET_KIND.list:
- return ;
+ return ;
default:
return ;
}
diff --git a/src/libs/ark/components/note/child.tsx b/src/libs/ark/components/note/child.tsx
new file mode 100644
index 00000000..63c2e611
--- /dev/null
+++ b/src/libs/ark/components/note/child.tsx
@@ -0,0 +1,57 @@
+import { useQuery } from '@tanstack/react-query';
+import { useArk } from '@libs/ark/provider';
+import { NoteChildUser } from './childUser';
+
+export function NoteChild({ eventId, isRoot }: { eventId: string; isRoot?: boolean }) {
+ const ark = useArk();
+ const { isLoading, isError, data } = useQuery({
+ queryKey: ['event', eventId],
+ queryFn: async () => {
+ // get event from relay
+ const event = await ark.getEventById({ id: eventId });
+
+ if (!event)
+ throw new Error(
+ `Cannot get event with ${eventId}, will be retry after 10 seconds`
+ );
+
+ return event;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ retry: 2,
+ });
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+ Failed to fetch event
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/libs/ark/components/note/childUser.tsx b/src/libs/ark/components/note/childUser.tsx
new file mode 100644
index 00000000..45b4f66d
--- /dev/null
+++ b/src/libs/ark/components/note/childUser.tsx
@@ -0,0 +1,86 @@
+import * as Avatar from '@radix-ui/react-avatar';
+import { useQuery } from '@tanstack/react-query';
+import { minidenticon } from 'minidenticons';
+import { useMemo } from 'react';
+import { useArk } from '@libs/ark/provider';
+import { displayNpub } from '@utils/formater';
+
+export function NoteChildUser({ pubkey, subtext }: { pubkey: string; subtext: string }) {
+ const ark = useArk();
+ const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]);
+ const fallbackAvatar = useMemo(
+ () => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)),
+ [pubkey]
+ );
+
+ const { isLoading, data: user } = useQuery({
+ queryKey: ['user', pubkey],
+ queryFn: async () => {
+ try {
+ const profile = await ark.getUserProfile({ pubkey });
+
+ if (!profile)
+ throw new Error(
+ `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`
+ );
+
+ return profile;
+ } catch (e) {
+ throw new Error(e);
+ }
+ },
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ retry: 2,
+ });
+
+ if (isLoading) {
+ return (
+ <>
+
+
+
+
+
{fallbackName}
+
+ {subtext}:
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {user?.display_name || user?.name || user?.displayName || fallbackName}{' '}
+
+
+ {subtext}:
+
+
+ >
+ );
+}
diff --git a/src/libs/ark/components/note/content.tsx b/src/libs/ark/components/note/content.tsx
new file mode 100644
index 00000000..647dfada
--- /dev/null
+++ b/src/libs/ark/components/note/content.tsx
@@ -0,0 +1,11 @@
+import { useRichContent } from '@utils/hooks/useRichContent';
+
+export function NoteContent({ content }: { content: string }) {
+ const { parsedContent } = useRichContent(content);
+
+ return (
+
+ {parsedContent}
+
+ );
+}
diff --git a/src/libs/ark/components/note/index.ts b/src/libs/ark/components/note/index.ts
new file mode 100644
index 00000000..f8a89f84
--- /dev/null
+++ b/src/libs/ark/components/note/index.ts
@@ -0,0 +1,19 @@
+import { NoteChild } from './child';
+import { NoteContent } from './content';
+import { NoteReaction } from './reaction';
+import { NoteReply } from './reply';
+import { NoteRepost } from './repost';
+import { NoteRoot } from './root';
+import { NoteUser } from './user';
+import { NoteZap } from './zap';
+
+export const Note = {
+ Root: NoteRoot,
+ User: NoteUser,
+ Content: NoteContent,
+ Reply: NoteReply,
+ Repost: NoteRepost,
+ Reaction: NoteReaction,
+ Zap: NoteZap,
+ Child: NoteChild,
+};
diff --git a/src/libs/ark/components/note/kinds/text.tsx b/src/libs/ark/components/note/kinds/text.tsx
new file mode 100644
index 00000000..edb45443
--- /dev/null
+++ b/src/libs/ark/components/note/kinds/text.tsx
@@ -0,0 +1,40 @@
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import { useArk } from '@libs/ark/provider';
+import { Note } from '..';
+
+export function TextNote({ event }: { event: NDKEvent }) {
+ const ark = useArk();
+ const thread = ark.getEventThread({ tags: event.tags });
+
+ return (
+
+
+ {thread ? (
+
+
+ {thread.rootEventId ? (
+
+ ) : null}
+ {thread.replyEventId ? : null}
+
+
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/src/libs/ark/components/note/reaction.tsx b/src/libs/ark/components/note/reaction.tsx
new file mode 100644
index 00000000..5cbc7ff9
--- /dev/null
+++ b/src/libs/ark/components/note/reaction.tsx
@@ -0,0 +1,122 @@
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import * as Popover from '@radix-ui/react-popover';
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { ReactionIcon } from '@shared/icons';
+
+const REACTIONS = [
+ {
+ content: '👏',
+ img: '/clapping_hands.png',
+ },
+ {
+ content: '🤪',
+ img: '/face_with_tongue.png',
+ },
+ {
+ content: '😮',
+ img: '/face_with_open_mouth.png',
+ },
+ {
+ content: '😢',
+ img: '/crying_face.png',
+ },
+ {
+ content: '🤡',
+ img: '/clown_face.png',
+ },
+];
+
+export function NoteReaction({ event }: { event: NDKEvent }) {
+ const [open, setOpen] = useState(false);
+ const [reaction, setReaction] = useState(null);
+
+ const getReactionImage = (content: string) => {
+ const reaction: { img: string } = REACTIONS.find((el) => el.content === content);
+ return reaction.img;
+ };
+
+ const react = async (content: string) => {
+ try {
+ setReaction(content);
+
+ // react
+ await event.react(content);
+
+ setOpen(false);
+ } catch (e) {
+ toast.error(e);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/libs/ark/components/note/reply.tsx b/src/libs/ark/components/note/reply.tsx
new file mode 100644
index 00000000..cc7dbd51
--- /dev/null
+++ b/src/libs/ark/components/note/reply.tsx
@@ -0,0 +1,43 @@
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { createSearchParams, useNavigate } from 'react-router-dom';
+import { ReplyIcon } from '@shared/icons';
+
+export function NoteReply({
+ eventId,
+ rootEventId,
+}: {
+ eventId: string;
+ rootEventId?: string;
+}) {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+
+
+ Quick reply
+
+
+
+
+
+ );
+}
diff --git a/src/libs/ark/components/note/repost.tsx b/src/libs/ark/components/note/repost.tsx
new file mode 100644
index 00000000..35c54751
--- /dev/null
+++ b/src/libs/ark/components/note/repost.tsx
@@ -0,0 +1,50 @@
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { twMerge } from 'tailwind-merge';
+import { RepostIcon } from '@shared/icons';
+
+export function NoteRepost({ event }: { event: NDKEvent }) {
+ const [isRepost, setIsRepost] = useState(false);
+
+ const submit = async () => {
+ try {
+ // repost
+ await event.repost(true);
+
+ // update state
+ setIsRepost(true);
+ toast.success("You've reposted this post successfully");
+ } catch (e) {
+ toast.error('Repost failed, try again later');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ Repost
+
+
+
+
+
+ );
+}
diff --git a/src/libs/ark/components/note/root.tsx b/src/libs/ark/components/note/root.tsx
new file mode 100644
index 00000000..59591e01
--- /dev/null
+++ b/src/libs/ark/components/note/root.tsx
@@ -0,0 +1,18 @@
+import { ReactNode } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+export function NoteRoot({
+ children,
+ className,
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/libs/ark/components/note/user.tsx b/src/libs/ark/components/note/user.tsx
new file mode 100644
index 00000000..8eb31e79
--- /dev/null
+++ b/src/libs/ark/components/note/user.tsx
@@ -0,0 +1,143 @@
+import * as Avatar from '@radix-ui/react-avatar';
+import { useQuery } from '@tanstack/react-query';
+import { minidenticon } from 'minidenticons';
+import { useMemo } from 'react';
+import { useArk } from '@libs/ark';
+import { RepostIcon } from '@shared/icons';
+import { displayNpub, formatCreatedAt } from '@utils/formater';
+
+export function NoteUser({
+ pubkey,
+ time,
+ variant = 'text',
+}: {
+ pubkey: string;
+ time: number;
+ variant?: 'text' | 'repost';
+}) {
+ const ark = useArk();
+ const createdAt = useMemo(() => formatCreatedAt(time), [time]);
+ const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]);
+ const fallbackAvatar = useMemo(
+ () => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)),
+ [pubkey]
+ );
+
+ const { isLoading, data: user } = useQuery({
+ queryKey: ['user', pubkey],
+ queryFn: async () => {
+ try {
+ const profile = await ark.getUserProfile({ pubkey });
+
+ if (!profile)
+ throw new Error(
+ `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`
+ );
+
+ return profile;
+ } catch (e) {
+ throw new Error(e);
+ }
+ },
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ retry: 2,
+ });
+
+ if (variant === 'repost') {
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {user?.name || user?.display_name || user?.displayName || fallbackName}
+
+ reposted
+
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {user?.name || user?.display_name || user?.displayName || fallbackName}
+
+
+
+
+ );
+}
diff --git a/src/libs/ark/components/note/zap.tsx b/src/libs/ark/components/note/zap.tsx
new file mode 100644
index 00000000..fcab79b3
--- /dev/null
+++ b/src/libs/ark/components/note/zap.tsx
@@ -0,0 +1,252 @@
+import { webln } from '@getalby/sdk';
+import { SendPaymentResponse } from '@getalby/sdk/dist/types';
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import * as Dialog from '@radix-ui/react-dialog';
+import { invoke } from '@tauri-apps/api/primitives';
+import { message } from '@tauri-apps/plugin-dialog';
+import { QRCodeSVG } from 'qrcode.react';
+import { useEffect, useRef, useState } from 'react';
+import CurrencyInput from 'react-currency-input-field';
+import { useNavigate } from 'react-router-dom';
+import { useArk } from '@libs/ark';
+import { CancelIcon, ZapIcon } from '@shared/icons';
+import { compactNumber, displayNpub } from '@utils/formater';
+import { useProfile } from '@utils/hooks/useProfile';
+import { sendNativeNotification } from '@utils/notification';
+
+export function NoteZap({ event }: { event: NDKEvent }) {
+ const [walletConnectURL, setWalletConnectURL] = useState(null);
+ const [amount, setAmount] = useState('21');
+ const [zapMessage, setZapMessage] = useState('');
+ const [invoice, setInvoice] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isCompleted, setIsCompleted] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { user } = useProfile(event.pubkey);
+
+ const ark = useArk();
+ const nwc = useRef(null);
+ const navigate = useNavigate();
+
+ const createZapRequest = async () => {
+ try {
+ if (!ark.readyToSign) return navigate('/new/privkey');
+
+ const zapAmount = parseInt(amount) * 1000;
+ const res = await event.zap(zapAmount, zapMessage);
+
+ if (!res)
+ return await message('Cannot create zap request', {
+ title: 'Zap',
+ type: 'error',
+ });
+
+ // user don't connect nwc, create QR Code for invoice
+ if (!walletConnectURL) return setInvoice(res);
+
+ // user connect nwc
+ nwc.current = new webln.NostrWebLNProvider({
+ nostrWalletConnectUrl: walletConnectURL,
+ });
+ await nwc.current.enable();
+
+ // start loading
+ setIsLoading(true);
+ // send payment via nwc
+ const send: SendPaymentResponse = await nwc.current.sendPayment(res);
+
+ if (send) {
+ await sendNativeNotification(
+ `You've tipped ${compactNumber.format(send.amount)} sats to ${
+ user?.name || user?.display_name || user?.displayName
+ }`
+ );
+
+ // eose
+ nwc.current.close();
+ setIsCompleted(true);
+ setIsLoading(false);
+
+ // reset after 3 secs
+ const timeout = setTimeout(() => setIsCompleted(false), 3000);
+ clearTimeout(timeout);
+ }
+ } catch (e) {
+ nwc.current.close();
+ setIsLoading(false);
+ await message(JSON.stringify(e), { title: 'Zap', type: 'error' });
+ }
+ };
+
+ useEffect(() => {
+ async function getWalletConnectURL() {
+ const uri: string = await invoke('secure_load', {
+ key: `${ark.account.pubkey}-nwc`,
+ });
+ if (uri) setWalletConnectURL(uri);
+ }
+
+ if (isOpen) getWalletConnectURL();
+
+ return () => {
+ setAmount('21');
+ setZapMessage('');
+ setIsCompleted(false);
+ setIsLoading(false);
+ };
+ }, [isOpen]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Send tip to{' '}
+ {user?.name || user?.displayName || displayNpub(event.pubkey, 16)}
+
+
+
+
+
+
+ {!invoice ? (
+ <>
+
+
+ setAmount(value)}
+ className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
+ />
+
+ sats
+
+
+
+
+
+
+
+
+
+
+
+
setZapMessage(e.target.value)}
+ spellCheck={false}
+ autoComplete="off"
+ autoCorrect="off"
+ autoCapitalize="off"
+ placeholder="Enter message (optional)"
+ className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
+ />
+
+ {walletConnectURL ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ ) : (
+
+
+
+
+
+
Scan to zap
+
+ You must use Bitcoin wallet which support Lightning
+
+ such as: Blue Wallet, Bitkit, Phoenix,...
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/libs/ark/index.ts b/src/libs/ark/index.ts
index a988345e..48c8ef3b 100644
--- a/src/libs/ark/index.ts
+++ b/src/libs/ark/index.ts
@@ -1,6 +1,5 @@
export * from './ark';
export * from './provider';
export * from './components/widget';
-export * from './components/widget/content';
-export * from './components/widget/header';
-export * from './components/widget/root';
+export * from './components/note';
+export * from './components/note/kinds/text';
diff --git a/src/shared/icons/horizontalDots.tsx b/src/shared/icons/horizontalDots.tsx
index 19eecef7..de8974e1 100644
--- a/src/shared/icons/horizontalDots.tsx
+++ b/src/shared/icons/horizontalDots.tsx
@@ -1,24 +1,20 @@
-import { SVGProps } from 'react';
-
-export function HorizontalDotsIcon(
- props: JSX.IntrinsicAttributes & SVGProps
-) {
+export function HorizontalDotsIcon(props: JSX.IntrinsicElements['svg']) {
return (
);
}
diff --git a/src/shared/icons/reaction.tsx b/src/shared/icons/reaction.tsx
index 2d16f037..ff73ba22 100644
--- a/src/shared/icons/reaction.tsx
+++ b/src/shared/icons/reaction.tsx
@@ -1,25 +1,18 @@
-import { SVGProps } from 'react';
-
-export function ReactionIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+export function ReactionIcon(props: JSX.IntrinsicElements['svg']) {
return (
);
}
diff --git a/src/shared/icons/reply.tsx b/src/shared/icons/reply.tsx
index a85c623b..2e781c96 100644
--- a/src/shared/icons/reply.tsx
+++ b/src/shared/icons/reply.tsx
@@ -1,29 +1,18 @@
-import { SVGProps } from 'react';
-
-export function ReplyIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+export function ReplyIcon(props: JSX.IntrinsicElements['svg']) {
return (
);
}
diff --git a/src/shared/icons/repost.tsx b/src/shared/icons/repost.tsx
index e0a93d6d..bd2ef69c 100644
--- a/src/shared/icons/repost.tsx
+++ b/src/shared/icons/repost.tsx
@@ -1,19 +1,18 @@
-import { SVGProps } from 'react';
-
-export function RepostIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+export function RepostIcon(props: JSX.IntrinsicElements['svg']) {
return (
);
}
diff --git a/src/shared/icons/zap.tsx b/src/shared/icons/zap.tsx
index 39633453..56a8ca29 100644
--- a/src/shared/icons/zap.tsx
+++ b/src/shared/icons/zap.tsx
@@ -1,22 +1,18 @@
-import { SVGProps } from 'react';
-
-export function ZapIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+export function ZapIcon(props: JSX.IntrinsicElements['svg']) {
return (
);
}
diff --git a/src/shared/notes/text.tsx b/src/shared/notes/text.tsx
index 007cc336..b439962e 100644
--- a/src/shared/notes/text.tsx
+++ b/src/shared/notes/text.tsx
@@ -16,7 +16,7 @@ export function TextNote({ event, className }: { event: NDKEvent; className?: st
const thread = ark.getEventThread({ tags: event.tags });
return (
-