don't hate me, old git is fuck up

This commit is contained in:
Ren Amamiya
2023-02-21 14:58:47 +07:00
commit 672298daf9
103 changed files with 12172 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { currentUser } from '@stores/currentUser';
import LikeIcon from '@assets/icons/Like';
import LikeSolidIcon from '@assets/icons/LikeSolid';
import { useStore } from '@nanostores/react';
import { dateToUnix, useNostr, useNostrEvents } from 'nostr-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useState } from 'react';
export default function Reaction({
eventID,
eventPubkey,
}: {
eventID: string;
eventPubkey: string;
}) {
const { publish } = useNostr();
const [reaction, setReaction] = useState(0);
const [isReact, setIsReact] = useState(false);
const $currentUser: any = useStore(currentUser);
const pubkey = $currentUser.pubkey;
const privkey = $currentUser.privkey;
const { onEvent } = useNostrEvents({
filter: {
'#e': [eventID],
since: 0,
kinds: [7],
limit: 20,
},
});
onEvent((rawMetadata) => {
try {
const content = rawMetadata.content;
if (content === '🤙' || content === '+') {
setReaction(reaction + 1);
}
} catch (err) {
console.error(err, rawMetadata);
}
});
const handleReaction = (e: any) => {
e.stopPropagation();
const event: any = {
content: '+',
kind: 7,
tags: [
['e', eventID],
['p', eventPubkey],
],
created_at: dateToUnix(),
pubkey: pubkey,
};
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
publish(event);
setIsReact(true);
setReaction(reaction + 1);
};
return (
<button
onClick={(e) => handleReaction(e)}
className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-800">
{isReact ? (
<LikeSolidIcon className="h-5 w-5 text-red-500" />
) : (
<LikeIcon className="h-5 w-5" />
)}
</div>
<span>{reaction}</span>
</button>
);
}

View File

@@ -0,0 +1,23 @@
import ReplyIcon from '@assets/icons/Reply';
import { useNostrEvents } from 'nostr-react';
export default function Reply({ eventID }: { eventID: string }) {
const { events } = useNostrEvents({
filter: {
'#e': [eventID],
since: 0,
kinds: [1],
limit: 10,
},
});
return (
<button className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-800">
<ReplyIcon />
</div>
<span>{events.length || 0}</span>
</button>
);
}

View File

@@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { truncate } from '@utils/truncate';
import { useNostrEvents } from 'nostr-react';
export default function RootUser({ userPubkey, action }: { userPubkey: string; action: string }) {
const { events } = useNostrEvents({
filter: {
authors: [userPubkey],
kinds: [0],
},
});
if (events !== undefined && events.length > 0) {
const userData: any = JSON.parse(events[0].content);
return (
<div className="text-zinc-400">
<p>
{userData?.name ? userData.name : truncate(userPubkey, 16, ' .... ')} {action}
</p>
</div>
);
} else {
return <></>;
}
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ImageWithFallback } from '@components/imageWithFallback';
import { truncate } from '@utils/truncate';
import MoreIcon from '@assets/icons/More';
import Avatar from 'boring-avatars';
import { useNostrEvents } from 'nostr-react';
import { memo } from 'react';
import Moment from 'react-moment';
export const User = memo(function User({ pubkey, time }: { pubkey: string; time: any }) {
const { events } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [0],
},
});
if (events !== undefined && events.length > 0) {
const userData: any = JSON.parse(events[0].content);
return (
<div className="relative flex items-start gap-4">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
{userData?.picture ? (
<ImageWithFallback
src={userData.picture}
alt={pubkey}
fill={true}
className="rounded-full object-cover"
/>
) : (
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
)}
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex items-center gap-2 text-sm">
<span className="font-bold leading-tight">
{userData?.name ? userData.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="text-zinc-500">·</span>
<Moment fromNow unix className="text-zinc-500">
{time}
</Moment>
</div>
<div>
<MoreIcon />
</div>
</div>
</div>
</div>
);
} else {
return (
<div className="relative flex animate-pulse items-start gap-4">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10 bg-zinc-700"></div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-16 rounded bg-zinc-700" />
</div>
<div>
<MoreIcon />
</div>
</div>
</div>
</div>
);
}
});

View File

@@ -0,0 +1,20 @@
import Image from 'next/image';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function ImagePreview({ data }: { data: object }) {
return (
<div
className={`relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-800`}>
<div className="relative h-full w-full">
<Image
src={data['image']}
alt="image preview"
width="0"
height="0"
sizes="100vw"
className="h-auto w-full object-cover"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import dynamic from 'next/dynamic';
import { useCallback, useEffect, useMemo, useRef } from 'react';
const MarkdownPreview = dynamic<MarkdownPreviewProps>(() => import('@uiw/react-markdown-preview'), {
ssr: false,
});
export default function Content({ data }: { data: string }) {
const imagesRef = useRef([]);
const videosRef = useRef([]);
const urls = useMemo(
() =>
data.match(
/((http|ftp|https):\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi
),
[data]
);
const extractURL = useCallback((urls: any[]) => {
if (urls !== null && urls.length > 0) {
urls.forEach((url: string | URL) => {
const parseURL = new URL(url);
const path = parseURL.pathname.toLowerCase();
switch (path) {
case path.match(/\.(jpg|jpeg|gif|png|webp)$/)?.input:
imagesRef.current.push(parseURL.href);
break;
case path.match(
/(http:|https:)?\/\/(www\.)?(youtube.com|youtu.be)\/(watch)?(\?v=)?(\S+)?/
)?.input:
videosRef.current.push(parseURL.href);
break;
case path.match(/\.(mp4|webm|m4v|mov|avi|mkv|flv)$/)?.input:
videosRef.current.push(parseURL.href);
break;
default:
break;
}
});
}
}, []);
useEffect(() => {
extractURL(urls);
}, [extractURL, urls]);
return (
<div className="flex flex-col">
<MarkdownPreview
source={data}
className={
'prose prose-zinc max-w-none break-words dark:prose-invert prose-headings:mt-3 prose-headings:mb-2 prose-p:m-0 prose-p:leading-normal prose-ul:mt-2 prose-li:my-1'
}
linkTarget="_blank"
disallowedElements={[
'Table',
'Heading ID',
'Highlight',
'Fenced Code Block',
'Footnote',
'Definition List',
'Task List',
]}
/>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import Image from 'next/image';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function ImageCard({ data }: { data: object }) {
return (
<div
className={`relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-800`}>
<div className="relative h-full w-full">
<Image
src={data['image']}
alt="image preview"
width="0"
height="0"
sizes="100vw"
className="h-auto w-full object-cover"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import Image from 'next/image';
import Link from 'next/link';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function LinkCard({ data }: { data: object }) {
return (
<Link
href={data['url']}
target={'_blank'}
className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-700">
<div className="relative aspect-video h-auto w-full">
<Image src={data['image']} alt="image preview" fill={true} className="object-cover" />
</div>
<div className="flex flex-col gap-2 p-4">
<div>
<h5 className="font-semibold leading-tight">{data['title']}</h5>
<p className="text-sm text-zinc-300">{data['description']}</p>
</div>
<span className="text-sm text-zinc-500">{data['url']}</span>
</div>
</Link>
);
}

View File

@@ -0,0 +1,17 @@
import ReactPlayer from 'react-player/lazy';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function Video({ data }: { data: object }) {
return (
<div className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-800">
<ReactPlayer
url={data['url']}
controls={true}
volume={0}
className="aspect-video w-full"
width="100%"
height="100%"
/>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import Reaction from '@components/note/atoms/reaction';
import Reply from '@components/note/atoms/reply';
import RootUser from '@components/note/atoms/rootUser';
import User from '@components/note/atoms/user';
import { Placeholder } from '@components/note/placeholder';
import LikeSolidIcon from '@assets/icons/LikeSolid';
import dynamic from 'next/dynamic';
import { useNostrEvents } from 'nostr-react';
import { memo } from 'react';
const DynamicContent = dynamic(() => import('@components/note/content'), {
ssr: false,
loading: () => (
<>
<p>Loading...</p>
</>
),
});
export const Liked = memo(function Liked({
eventUser,
sourceID,
}: {
eventUser: string;
sourceID: string;
}) {
const { events } = useNostrEvents({
filter: {
ids: [sourceID],
since: 0,
kinds: [1],
limit: 1,
},
});
if (events !== undefined && events.length > 0) {
return (
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
<div className="flex items-center gap-1 pl-8 text-sm">
<LikeSolidIcon className="h-4 w-4 text-zinc-400" />
<div className="ml-2">
<RootUser userPubkey={eventUser} action={'like'} />
</div>
</div>
<div className="flex flex-col">
<User pubkey={events[0].pubkey} time={events[0].created_at} />
<div className="-mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<DynamicContent data={events[0].content} />
<div className="-ml-1 flex items-center gap-8">
<Reply eventID={events[0].id} />
<Reaction eventID={events[0].id} eventPubkey={events[0].pubkey} />
</div>
</div>
</div>
</div>
</div>
);
} else {
return <Placeholder />;
}
});

View File

@@ -0,0 +1,64 @@
import Reaction from '@components/note/atoms/reaction';
import Reply from '@components/note/atoms/reply';
import User from '@components/note/atoms/user';
import { Placeholder } from '@components/note/placeholder';
import dynamic from 'next/dynamic';
import { useNostrEvents } from 'nostr-react';
const DynamicContent = dynamic(() => import('@components/note/content'), {
ssr: false,
loading: () => (
<>
<p>Loading...</p>
</>
),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function Multi({ event }: { event: any }) {
const tags = event.tags;
const { events } = useNostrEvents({
filter: {
ids: [tags[0][1]],
since: 0,
kinds: [1],
limit: 1,
},
});
if (events !== undefined && events.length > 0) {
return (
<div className="relative flex h-min min-h-min w-full select-text flex-col overflow-hidden border-b border-zinc-800">
<div className="absolute left-[45px] top-6 h-full w-[2px] bg-zinc-800"></div>
<div className="flex flex-col bg-zinc-900 px-6 pt-6 pb-2">
<User pubkey={events[0].pubkey} time={events[0].created_at} />
<div className="-mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<DynamicContent data={events[0].content} />
<div className="-ml-1 flex items-center gap-8">
<Reply eventID={events[0].id} />
<Reaction eventID={events[0].id} eventPubkey={events[0].pubkey} />
</div>
</div>
</div>
</div>
<div className="relative flex flex-col bg-zinc-900 px-6 pb-6">
<User pubkey={event.pubkey} time={event.created_at} />
<div className="relative z-10 -mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<DynamicContent data={event.content} />
<div className="-ml-1 flex items-center gap-8">
<Reply eventID={event.id} />
<Reaction eventID={event.id} eventPubkey={event.pubkey} />
</div>
</div>
</div>
</div>
</div>
);
} else {
return <Placeholder />;
}
}

View File

@@ -0,0 +1,30 @@
import { memo } from 'react';
export const Placeholder = memo(function Placeholder() {
return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col py-6 px-6">
<div className="flex items-start gap-4">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
<div className="h-3 w-3 rounded-full bg-zinc-700" />
</div>
</div>
</div>
<div className="-mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,64 @@
import Reaction from '@components/note/atoms/reaction';
import Reply from '@components/note/atoms/reply';
import RootUser from '@components/note/atoms/rootUser';
import User from '@components/note/atoms/user';
import { Placeholder } from '@components/note/placeholder';
import RepostIcon from '@assets/icons/Repost';
import dynamic from 'next/dynamic';
import { useNostrEvents } from 'nostr-react';
import { memo } from 'react';
const DynamicContent = dynamic(() => import('@components/note/content'), {
ssr: false,
loading: () => (
<>
<p>Loading...</p>
</>
),
});
export const Repost = memo(function Repost({
eventUser,
sourceID,
}: {
eventUser: string;
sourceID: string;
}) {
const { events } = useNostrEvents({
filter: {
ids: [sourceID],
since: 0,
kinds: [1],
limit: 1,
},
});
if (events !== undefined && events.length > 0) {
return (
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
<div className="flex items-center gap-1 pl-8 text-sm">
<RepostIcon className="h-4 w-4 text-zinc-400" />
<div className="ml-2">
<RootUser userPubkey={eventUser} action={'repost'} />
</div>
</div>
<div className="flex flex-col">
<User pubkey={events[0].pubkey} time={events[0].created_at} />
<div className="-mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<DynamicContent data={events[0].content} />
<div className="-ml-1 flex items-center gap-8">
<Reply eventID={events[0].id} />
<Reaction eventID={events[0].id} eventPubkey={events[0].pubkey} />
</div>
</div>
</div>
</div>
</div>
);
} else {
return <Placeholder />;
}
});

View File

@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Reaction from '@components/note/atoms/reaction';
import Reply from '@components/note/atoms/reply';
import { User } from '@components/note/atoms/user';
import dynamic from 'next/dynamic';
import { memo } from 'react';
const DynamicContent = dynamic(() => import('@components/note/content'), {
ssr: false,
loading: () => (
<>
<p>Loading...</p>
</>
),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Single = memo(function Single({ event }: { event: any }) {
return (
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
<div className="flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} />
<div className="-mt-4 pl-[60px]">
<div className="flex flex-col gap-2">
<DynamicContent data={event.content} />
<div className="-ml-1 flex items-center gap-8">
<Reply eventID={event.id} />
<Reaction eventID={event.id} eventPubkey={event.pubkey} />
</div>
</div>
</div>
</div>
</div>
);
});