should fix

This commit is contained in:
smolgrrr 2024-08-16 14:32:47 +10:00
parent 85ba016ef7
commit 6f7d41edd0
32 changed files with 0 additions and 270227 deletions

View File

@ -1,57 +0,0 @@
import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { RefObject } from "react";
import customEmojis from "../custom_emojis.json";
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
export function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = customEmojis.map((pack) => {
return {
id: pack.id,
name: pack.name,
emojis: pack.emojis
.filter((e) => !e.static_url.endsWith('.svg'))
.map((e) => {
return {
id: e.shortcode,
name: e.shortcode,
skins: [{ src: e.static_url }],
};
}),
};
});
return (
<>
<div className="absolute z-25" ref={ref}>
<Picker
autoFocus
custom={customEmojiList}
data = {data}
perLine={7}
previewPosition="none"
skinTonePosition="none"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
categories={['poast']}
/>
</div>
</>
);
}

View File

@ -1,215 +0,0 @@
import {
ServerIcon,
CpuChipIcon,
ArrowPathIcon,
PlusCircleIcon
} from "@heroicons/react/24/outline";
import { XCircleIcon } from "@heroicons/react/24/solid";
import { useState, useEffect, useRef } from "react";
import { UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { renderMedia, attachFile } from "../../utils/FileUpload";
import EmojiPicker from "@emoji-mart/react";
import customEmojis from './custom_emojis.json';
import { useSubmitForm } from "./handleSubmit";
import "../../styles/Form.css";
interface FormProps {
refEvent?: NostrEvent;
tagType?: 'Reply' | 'Quote' | '';
hashtag?: string;
}
const NewNoteCard = ({
refEvent,
tagType,
hashtag,
}: FormProps) => {
const ref = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState("");
const [file, setFile] = useState("");
const [unsigned, setUnsigned] = useState<UnsignedEvent>({
kind: 1,
tags: [
[
"client",
"getwired.app"
]
],
content: "",
created_at: Math.floor(Date.now() / 1000),
pubkey: "",
});
const [difficulty, setDifficulty] = useState(
localStorage.getItem("difficulty") || "21"
);
const [fileSizeError, setFileSizeError] = useState(false);
const [uploadingFile, setUploadingFile] = useState(false);
useEffect(() => {
if (hashtag) {
unsigned.tags.push(['t', hashtag as string]);
}
if (refEvent && tagType) {
unsigned.tags = Array.from(new Set(unsigned.tags.concat(refEvent.tags)));
unsigned.tags.push(['p', refEvent.pubkey]);
if (tagType === 'Reply') {
unsigned.tags.push(['e', refEvent.id, refEvent.tags.some(tag => tag[0] === 'e') ? 'root' : '']);
} else {
if (tagType === 'Quote') {
setComment(comment + '\nnostr:' + nip19.noteEncode(refEvent.id));
unsigned.tags.push(['q', refEvent.id]);
} else {
unsigned.tags.push(['e', refEvent.id]);
}
}
}
const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail;
setDifficulty(difficulty);
};
window.addEventListener("difficultyChanged", handleDifficultyChange);
return () => {
window.removeEventListener("difficultyChanged", handleDifficultyChange);
};
}, []);
useEffect(() => {
setUnsigned(prevUnsigned => ({
...prevUnsigned,
content: `${comment} ${file}`,
created_at: Math.floor(Date.now() / 1000),
}));
}, [comment, file]);
const { handleSubmit: originalHandleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
const handleSubmit = async (event: React.FormEvent) => {
await originalHandleSubmit(event);
setComment("");
setFile("");
setUnsigned(prevUnsigned => ({
...prevUnsigned,
content: '',
created_at: Math.floor(Date.now() / 1000)
}));
};
//Emoji stuff
const emojiRef = useRef(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
interface Emoji {
native?: string;
id?: string;
}
const emojiNames = customEmojis.map(p => p.emojis).flat();
function getEmojiById(id: string) {
return emojiNames.find(e => e.shortcode === id);
}
async function onEmojiSelect(emoji: Emoji) {
setShowEmojiPicker(false);
try {
if (emoji.id) {
const e = getEmojiById(emoji.id);
if (e) {
setComment(comment + " :" + e.shortcode + ":");
unsigned.tags.push(['emoji', e.shortcode, e.url]);
};
}
} catch {
//ignore
}
}
const topOffset = ref.current?.getBoundingClientRect().top;
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(e: React.MouseEvent) {
e.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<form
name="post"
method="post"
encType="multipart/form-data"
className=""
onSubmit={handleSubmit}
>
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={2.5 * 1024 * 1024} />
<div className="px-4 flex flex-col rounded-lg">
<textarea
name="com"
wrap="soft"
className="shadow-lg w-full px-4 py-3 border-blue-500 bg-black text-white"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={comment.split('\n').length || 1}
/>
<div className="relative">
{file !== "" && (
<button onClick={() => setFile("")}>
<XCircleIcon className="h-10 w-10 absolute shadow z-100 text-blue-500" />
</button>
)}
{renderMedia([file])}
</div>
<div className="h-14 flex items-center justify-between">
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
<div className="inline-flex items-center gap-1.5 text-neutral-300">
<CpuChipIcon className="h-4 w-4" />
</div>
<p className="text-xs font-medium text-neutral-400">
{difficulty} Work
</p>
</div>
<div>
<div className="flex items-center gap-4">
<div className="items-center">
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset || 0}
leftOffset={leftOffset || 0}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
<PlusCircleIcon className="h-4 w-4 text-neutral-400 cursor-pointer" onClick={pickEmoji} />
</div>
<button
type="submit"
className={`bg-black border h-9 inline-flex items-center justify-center px-4 rounded-lg text-white font-medium text-sm ${doingWorkProp || uploadingFile ? 'cursor-not-allowed' : ''}`}
disabled={doingWorkProp || uploadingFile}
>
Submit
</button>
</div>
</div>
</div>
</div>
{fileSizeError ? (
<span className="text-red-500">File size should not exceed 2.5MB</span>
) : null}
{doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Doing Work:</span>
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
</div>
) : null}
<div id="postFormError" className="text-red-500" />
</form>
);
};
export default NewNoteCard;

View File

@ -1,86 +0,0 @@
import {
CpuChipIcon
} from "@heroicons/react/24/outline";
import { useState, useEffect } from "react";
import { UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { useSubmitForm } from "./handleSubmit";
import "../../styles/Form.css";
interface FormProps {
refEvent: NostrEvent;
}
const RepostNote = ({
refEvent
}: FormProps) => {
const [difficulty, setDifficulty] = useState(
localStorage.getItem("difficulty") || "21"
);
const [unsigned] = useState<UnsignedEvent>({
kind: 6,
tags: [
['client', 'getwired.app'],
['e', refEvent.id, 'wss://relay.damus.io'],
['p', refEvent.pubkey]
],
content: JSON.stringify(refEvent),
created_at: Math.floor(Date.now() / 1000),
pubkey: "",
});
useEffect(() => {
const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail;
setDifficulty(difficulty);
};
window.addEventListener("difficultyChanged", handleDifficultyChange);
return () => {
window.removeEventListener("difficultyChanged", handleDifficultyChange);
};
}, []);
const { handleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
return (
<form
name="post"
method="post"
encType="multipart/form-data"
className=""
onSubmit={handleSubmit}
>
<div className="px-4 flex flex-col rounded-lg">
<div className="h-14 flex items-center justify-between">
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
<div className="inline-flex items-center gap-1.5 text-neutral-300">
<CpuChipIcon className="h-4 w-4" />
</div>
<p className="text-xs font-medium text-neutral-400">
{difficulty} PoW
</p>
</div>
<button
type="submit"
className={`bg-black border h-9 inline-flex items-center justify-center px-4 rounded-lg text-white font-medium text-sm ${doingWorkProp ? 'cursor-not-allowed' : ''}`}
disabled={doingWorkProp}
>
Submit
</button>
</div>
</div>
{doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Doing Work:</span>
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
</div>
) : null}
<div id="postFormError" className="text-red-500" />
</form>
);
};
export default RepostNote;

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +0,0 @@
import { useState, useEffect } from "react";
import { generateSecretKey, getPublicKey, finalizeEvent, UnsignedEvent } from "nostr-tools";
import { publish } from "../../utils/relays";
const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => {
const [messageFromWorker, setMessageFromWorker] = useState(null);
const [doingWorkProgress, setDoingWorkProgress] = useState(0);
const startWork = () => {
const workers = Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url)));
workers.forEach((worker, index) => {
worker.onmessage = (event) => {
if (event.data.status === 'progress') {
console.log(`Worker progress: Checked ${event.data.currentNonce} nonces.`);
setDoingWorkProgress(event.data.currentNonce);
} else if (event.data.found) {
setMessageFromWorker(event.data.event);
// Terminate all workers once a solution is found
workers.forEach(w => w.terminate());
}
};
worker.postMessage({
unsigned,
difficulty,
nonceStart: index, // Each worker starts from its index
nonceStep: numCores // Each worker increments by the total number of workers
});
});
};
return { startWork, messageFromWorker, doingWorkProgress };
};
export const useSubmitForm = (unsigned: UnsignedEvent, difficulty: string) => {
const [doingWorkProp, setDoingWorkProp] = useState(false);
const [sk, setSk] = useState(generateSecretKey());
const unsignedWithPubkey = { ...unsigned, pubkey: getPublicKey(sk) };
const powServer = useState(localStorage.getItem('powserver') || '');
const [unsignedPoWEvent, setUnsignedPoWEvent] = useState<UnsignedEvent>()
let storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]');
// Initialize the worker outside of any effects
const numCores = navigator.hardwareConcurrency || 4;
const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsignedWithPubkey, difficulty, [unsignedWithPubkey]);
useEffect(() => {
if (unsignedPoWEvent) {
setDoingWorkProp(false);
const signedEvent = finalizeEvent(unsignedPoWEvent, sk);
publish(signedEvent);
setSk(generateSecretKey())
}
}, [unsignedPoWEvent]);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setDoingWorkProp(true);
console.log(powServer[0])
if (powServer[0]) {
const inEventFormat = { ...unsignedWithPubkey, sig: "" };
const powRequest = {
req_event: inEventFormat,
difficulty: difficulty
};
fetch(`${powServer[0]}/powgen`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(powRequest)
})
.then(response => response.json())
.then(data => {
console.log(data);
// handle the response data
setUnsignedPoWEvent(data.event)
})
.catch(error => {
console.error('Error:', error);
});
} else {
startWork();
}
// Add the logic here
storedKeys.push([sk, getPublicKey(sk)]);
// Stringify the array and store it back to localStorage
localStorage.setItem('usedKeys', JSON.stringify(storedKeys));
};
useEffect(() => {
if (messageFromWorker) {
setUnsignedPoWEvent(messageFromWorker);
}
}, [messageFromWorker]);
return { handleSubmit, doingWorkProp, doingWorkProgress };
};

View File

@ -1,9 +0,0 @@
import { PropsWithChildren } from "react";
export default function CardContainer({ children }: PropsWithChildren) {
return (
<div className="card break-inside-avoid mb-4 h-min">
<div className="card-body">{children}</div>
</div>
);
}

View File

@ -1,68 +0,0 @@
// import { getLinkPreview } from 'link-preview-js';
// import { useState, useEffect } from 'react';
const LinkModal = ({ url }: { url: string }) => {
// const [linkPreview, setLinkPreview] = useState<LinkPreview | null>(null);
// const [error, setError] = useState<string | null>(null);
// const fetchWithProxy = (url: string) => {
// const proxyUrl = 'https://api.allorigins.win/raw?url=';
// return getLinkPreview(proxyUrl + url)
// .then((preview) => setLinkPreview(preview as LinkPreview))
// .catch((error) => {
// console.error("Error fetching URL with proxy:", error);
// setError('Unable to fetch URL with proxy.');
// });
// };
// useEffect(() => {
// getLinkPreview(url)
// .then((preview) => setLinkPreview(preview as LinkPreview))
// .catch(error => {
// console.error("Error fetching original URL, trying with proxy:", error);
// setError('Error fetching original URL. Trying with proxy...');
// return fetchWithProxy(url);
// });
// }, [url]);
// if (error) {
// return <a className='hover:underline text-xs text-neutral-500' href={url}>{url}</a>; // or some loading state
// }
// if (!linkPreview) {
return <a className='hover:underline text-xs text-neutral-500' href={url}>{url}</a>; // or some loading state
// }
// return (
// <div className="link-preview p-1 bg-neutral-800 rounded-lg border border-neutral-800">
// <a href={linkPreview.url} target="_blank" rel="noopener noreferrer" className="">
// <img src={linkPreview.images[0]} alt={linkPreview.title} className="rounded-lg" />
// <div className="font-semibold text-xs text-gray-300">
// {linkPreview.title}
// </div>
// </a>
// </div>
// );
};
// interface LinkPreview {
// url: string;
// title: string;
// siteName?: string;
// description?: string;
// mediaType: string;
// contentType?: string;
// images: string[];
// videos: {
// url?: string;
// secureUrl?: string;
// type?: string;
// width?: string;
// height?: string;
// [key: string]: any;
// }[];
// [key: string]: any;
// }
export default LinkModal;

View File

@ -1,61 +0,0 @@
import { parseContent } from "../../../utils/content";
import { Event } from "nostr-tools";
import { getMetadata } from "../../../utils/getMetadata";
import ContentPreview from "./TextModal";
import { renderMedia } from "../../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../../utils/cardUtils";
import { CpuChipIcon } from "@heroicons/react/24/outline";
import { verifyPow } from "../../../utils/mine";
const QuoteEmbed = ({
key,
event,
metadata,
}: {
key?: string | number;
event: Event;
metadata: Event | null;
}) => {
const { files } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
let metadataParsed = null;
if (metadata !== null) {
metadataParsed = getMetadata(metadata);
}
return (
<div className="p-3 rounded-lg border border-neutral-700">
<div className="flex flex-col gap-2">
<div className="flex flex-col break-words">
<ContentPreview key={event.id} eventdata={event} />
</div>
<div className="flex justify-between items-center">
{metadataParsed ?
<img
key={key}
className={`h-5 w-5 rounded-full`}
src={metadataParsed?.picture ?? icon}
alt=""
loading="lazy"
decoding="async" />
:
<div className={`h-4 w-4 ${icon} rounded-full`} />
}
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(event)}
</div>
<span className="text-neutral-700">·</span>
<div className="text-xs font-semibold text-neutral-600">
{timeAgo(event.created_at)}
</div>
<span className="text-neutral-700">·</span>
</div>
</div>
</div>
</div>
);
};
export default QuoteEmbed;

View File

@ -1,102 +0,0 @@
import React from "react";
import { Event } from "nostr-tools";
import { useEffect, useState } from "react";
import { subNoteOnce } from "../../../utils/subscriptions";
import { nip19 } from "nostr-tools";
import { parseContent } from "../../../utils/content";
import QuoteEmbed from "./QuoteEmbed";
import LinkModal from "./LinkPreview";
const RichText = ({ text, isExpanded, emojiMap }: { text: string; isExpanded: boolean; emojiMap: Record<string, any> }) => {
const content = isExpanded ? text.split('\n') : text.slice(0, 350).split('\n');
return (
<>
{content.map((line, i) => (
<div key={i}>
{line.split(' ').map((word, j) =>
emojiMap[word]
? <img className="w-8 h-8 mx-0.5 inline" src={emojiMap[word]} alt={word} key={j} />
: `${word} `
)}
</div>
))}
</>
);
};
const ContentPreview = ({ key, eventdata }: { key: string; eventdata: Event }) => {
const { comment } = parseContent(eventdata);
const [finalComment, setFinalComment] = useState(comment);
const [quoteEvents, setQuoteEvents] = useState<Event[]>([]); // Initialize state
const [isExpanded, setIsExpanded] = useState(false);
const [url, setUrl] = useState("");
const [emojiMap, setEmojiMap] = useState<Record<string, any>>({});
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
setQuoteEvents((prevEvents) => [...prevEvents, event]);
};
useEffect(() => {
const findUrl = comment.match(/\bhttps?:\/\/\S+/gi);
if (findUrl && findUrl.length > 0) {
setUrl(findUrl[0]);
setFinalComment(finalComment.replace(findUrl[0], "").trim());
}
const match = comment.match(/\bnostr:([a-z0-9]+)/i);
const nostrQuoteID = match && match[1];
if (nostrQuoteID && nostrQuoteID.length > 0) {
let id_to_hex = String(nip19.decode(nostrQuoteID as string).data);
subNoteOnce(id_to_hex, onEvent);
setFinalComment(finalComment.replace("nostr:" + nostrQuoteID, "").trim());
}
let newEmojiMap: Record<string, any> = {};
eventdata.tags.forEach(tag => {
if (tag[0] === "emoji") {
newEmojiMap[`:${tag[1]}:`] = tag[2];
}
});
// Update the state variable
setEmojiMap(newEmojiMap);
}, [comment, finalComment]);
const getMetadataEvent = (event: Event) => {
const metadataEvent = quoteEvents.find(
(e) => e.pubkey === event.pubkey && e.kind === 0
);
if (metadataEvent) {
return metadataEvent;
}
return null;
};
return (
<div className="gap-2 flex flex-col break-words text-xs">
<RichText text={finalComment} isExpanded={isExpanded} emojiMap={emojiMap} />
{finalComment.length > 350 && (
<button
className="text-sm text-neutral-500"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? "...Read less" : "...Read more"}
</button>
)}
{url !== "" && <LinkModal key={key} url={url} />}
{quoteEvents[0] && quoteEvents.length > 0 && (
<a href={`/thread/${nip19.noteEncode(quoteEvents[0].id)}`}>
<QuoteEmbed
key={key}
event={quoteEvents[0]}
metadata={getMetadataEvent(quoteEvents[0])}
/>
</a>
)}
</div>
);
};
export default ContentPreview;

View File

@ -1,95 +0,0 @@
import React, { useState, useEffect } from 'react';
import { XMarkIcon } from "@heroicons/react/24/solid";
import { ArrowUpOnSquareIcon, PlusCircleIcon } from '@heroicons/react/24/outline';
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
declare global {
interface Navigator {
standalone?: boolean;
}
}
const AddToHomeScreenPrompt: React.FC = () => {
const [inMobileBrowser, setInMobileBrowser] = useState(false);
useEffect(() => {
const checkPWA = () => {
// Check if the app is running as a PWA on Android
const isAndroidPWA = window.matchMedia('(display-mode: standalone)').matches ||
window.matchMedia('(display-mode: minimal-ui)').matches;
// Check if the app is running as a PWA on other platforms
const isOtherPWA = window.navigator.standalone;
return !isAndroidPWA && !isOtherPWA;
};
// Function to detect mobile browser
const detectMobileBrowser = () => {
return (
(navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/Windows Phone/i))
);
};
const timer = setTimeout(() => {
setInMobileBrowser(Boolean(checkPWA() && detectMobileBrowser()));
}, 2000); // 3000 milliseconds = 3 seconds
// Cleanup function to clear the timeout if the component unmounts before the timeout finishes
return () => clearTimeout(timer);
}, []);
if (!inMobileBrowser) {
return null;
}
return (
<Transition appear show={inMobileBrowser} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={() => setInMobileBrowser(false)}
>
<div className="min-h-screen px-4 text-center">
<Dialog.Overlay className="fixed inset-0 bg-gray-800 opacity-40" />
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div className="fixed bottom-0 left-0 right-0 p-4 bg-neutral-900 rounded-lg m-2 border border-neutral-700 shadow-md flex justify-between items-center animate-slide-up">
<div className="flex flex-col text-white">
<span className="font-semibold">Stay Wired</span>
<p className="text-xs">Add Wired to your home screen for a better experience</p>
<ul className="list-none mt-2 text-sm">
<li>
<div className="flex items-center">
<span className="mr-2">{'>'}</span> Click on <ArrowUpOnSquareIcon className="h-6 w-6 ml-1 text-blue-500" /> <span className="font-semibold text-blue-500">Share</span>
</div>
</li>
<li>
<div className="flex items-center">
<span className="mr-2">{'>'}</span> Click <PlusCircleIcon className="h-6 w-6 ml-1 text-blue-500" /> <span className="font-semibold text-blue-500">Add to Home Screen</span>
</div>
</li>
</ul>
</div>
<button className="absolute top-2 right-2" onClick={() => {setInMobileBrowser(!inMobileBrowser);}}>
<XMarkIcon className="h-6 w-6 text-white" />
</button>
</div>
</div>
</Dialog>
</Transition>
);
};
export default AddToHomeScreenPrompt;

View File

@ -1,54 +0,0 @@
import {
Cog6ToothIcon,
BellIcon,
HashtagIcon
} from "@heroicons/react/24/outline";
import { useLocation } from "react-router-dom";
export default function Header() {
const location = useLocation();
const pathParts = location.pathname.split('/');
const secondLastPart = pathParts[pathParts.length - 2];
const lastPathPart = secondLastPart === "thread" ? "/thread" : "/" + pathParts[pathParts.length - 1];
return (
<header className="mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div className="flex justify-between items-center">
<a href="/">
<div className="flex items-center gap-2">
<img src="/icon.png" className="h-12" alt="logo" />
<span className="font-semibold text-white">
{`~/WIRED${lastPathPart}>`}
</span>
</div>
</a>
<div>
<a
href="/hashtags"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<HashtagIcon className="h-5 w-5" />
</button>
</a>
<a
href="/notifications"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<BellIcon className="h-5 w-5" />
</button>
</a>
<a
href="/settings"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<Cog6ToothIcon className="h-5 w-5" />
</button>
</a>
</div>
</div>
</header>
);
}

View File

@ -1,24 +0,0 @@
const Placeholder = () => {
return (
<div className="mx-auto gap-4 p-4">
<div className="border border-blue-300 shadow rounded-md p-4 max-w-sm w-full mx-auto">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-slate-700 h-10 w-10"></div>
<div className="flex-1 space-y-6 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
</div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Placeholder;

View File

@ -1,119 +0,0 @@
// import CardContainer from "./CardContainer";
import { CpuChipIcon } from "@heroicons/react/24/outline";
// import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools";
import { getMetadata, Metadata } from "../../utils/getMetadata";
// import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../utils/cardUtils";
import { verifyPow } from "../../utils/mine";
import { useNavigate } from 'react-router-dom';
import { subNoteOnce } from "../../utils/subscriptions";
import { useEffect, useState } from "react";
import ContentPreview from "./CardModals/TextModal";
interface RepostProps {
key?: string | number;
event: Event;
}
const RepostCard = ({
key,
event
}: RepostProps) => {
const repostedEvent = JSON.parse(event.content);
// const { files } = parseContent(repostedEvent);
const icon = getIconFromHash(event.pubkey);
const navigate = useNavigate();
const [cachedMetadataEvents, setCachedMetadataEvents] = useState<Event[]>(
JSON.parse(localStorage.getItem("cachedMetadataEvents") || "[]")
);
const [metadata, setMetadata] = useState<Metadata>()
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
const existingEvent = cachedMetadataEvents.find((e) => e.pubkey === event.pubkey)
if (existingEvent) {
setMetadata(getMetadata(existingEvent))
}
else if (!existingEvent && event.kind === 0 && event.pubkey === repostedEvent.pubkey && metadata == null) {
setMetadata(getMetadata(event))
setCachedMetadataEvents((prevMetadataEvents) => {
// Check if the event already exists in the cached metadata events
const existingEvent = prevMetadataEvents.find((e) => e.id === event.id || e.pubkey === event.pubkey)
if (!existingEvent) {
// If the event doesn't exist, add it to the cached metadata events
return [...prevMetadataEvents, event];
} else if (existingEvent && existingEvent.created_at < event.created_at) {
// Remove any existing metadata event with the same pubkey and id
const updatedMetadataEvents = prevMetadataEvents.filter(
(e) => e.id !== existingEvent.id
);
// Add the new metadata event
return [...updatedMetadataEvents, event];
}
// If the event already exists, return the previous cached metadata events
return prevMetadataEvents;
});
}
};
useEffect(() => {
subNoteOnce(repostedEvent.id, onEvent);
}, [repostedEvent.id]);
// Save the cached metadataEvents to localStorage
useEffect(() => {
localStorage.setItem("cachedMetadataEvents", JSON.stringify(cachedMetadataEvents));
}, [cachedMetadataEvents]);
const handleClick = () => {
navigate(`/thread/${nip19.noteEncode(repostedEvent.id)}`);
};
return (
<div>
<div className="ml-1 flex text-sm text-neutral-600 gap-2.5">
Repost
@
<span className="inline-flex"><CpuChipIcon className="h-5 w-5" /> {verifyPow(event)}</span>
</div>
<div className="rounded-lg border border-neutral-700">
<div className="card break-inside-avoid h-min">
<div className="card-body">
<div className={`flex flex-col gap-2`}>
<div className={`flex flex-col break-words hover:cursor-pointer`} onClick={handleClick}>
<ContentPreview key={repostedEvent.id} eventdata={repostedEvent} />
</div>
<div className={`flex justify-between items-center hover:cursor-pointer`} onClick={handleClick}>
{metadata ?
<img
key = {key}
className={`h-5 w-5 rounded-full`}
src={metadata?.picture ?? icon}
alt=""
loading="lazy"
decoding="async"/>
:
<div className={`h-4 w-4 ${icon} rounded-full`} />
}
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(repostedEvent)}
</div>
<span className="text-neutral-700">·</span>
<div className="text-xs font-semibold text-neutral-600">
{timeAgo(repostedEvent.created_at)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default RepostCard;

View File

@ -1,57 +0,0 @@
import PostCard from "../modals/PostCard";
import { verifyPow } from "../../utils/mine";
import { Event } from "nostr-tools";
import NewNoteCard from "../forms/PostFormCard";
import RepostCard from "../modals/RepostCard";
import { useParams } from "react-router-dom";
import { useUniqEvents } from "../../hooks/useUniqEvents";
const DEFAULT_DIFFICULTY = 0;
const HashtagPage = () => {
const { id } = useParams();
const filterDifficulty = localStorage.getItem("filterHashtagDifficulty") || DEFAULT_DIFFICULTY;
const { noteEvents, metadataEvents } = useUniqEvents(id as string, false);
const postEvents: Event[] = noteEvents
.filter((event) =>
verifyPow(event) >= Number(filterDifficulty) &&
event.kind !== 0 &&
(event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e"))
)
let sortedEvents = [...postEvents]
.sort((a, b) => {
// Sort by PoW in descending order
const powDiff = verifyPow(b) - verifyPow(a);
if (powDiff !== 0) return powDiff;
// If PoW is the same, sort by created_at in descending order
return b.created_at - a.created_at;
});
// Render the component
return (
<main className="text-white mb-20">
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard hashtag={id as string} />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
{sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={sortedEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))}
/>
:
<RepostCard
event={event}
/>
))}
</div>
</main>
);
};
export default HashtagPage;

View File

@ -1,71 +0,0 @@
import React, { useState } from 'react';
export const DefaultHashtags = ['asknostr', 'politics', 'technology', 'bitcoin', 'wired'];
const Hashtags = () => {
const [addedHashtags, setAddedHashtags] = useState<string[]>(JSON.parse(localStorage.getItem('hashtags') as string) || []);
const [newHashtag, setNewHashtag] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newHashtagArray = [...addedHashtags, newHashtag];
setAddedHashtags(newHashtagArray);
localStorage.setItem('addedBoards', JSON.stringify(newHashtagArray));
};
const clearBoards = () => {
localStorage.setItem('hashtags', JSON.stringify([]));
setAddedHashtags([]);
};
return (
<div className="settings-page bg-black text-white p-8 flex flex-col h-full">
<h1 className="text-lg font-semibold mb-4">Saved hashtags</h1>
<div className="">
{/* Map over DefaultBoards and addedBoards and display them */}
<ul className='py-4'>
{DefaultHashtags.map((hashtag, index) => (
<li key={index}><a href={`/hashtag/${hashtag}`} className='hover:underline'>#{hashtag}</a></li>
))}
{addedHashtags.map((hashtag, index: number) => (
<li key={index}><a href={`/hashtag/${hashtag}`} className='hover:underline'>#{hashtag}</a></li>
))}
</ul>
<form onSubmit={handleSubmit}>
<div className="flex flex-wrap -mx-2 my-4">
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="difficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Add Hashtag
</span>
</label>
<div className="flex">
<input
id="hashtag"
type="string"
placeholder={'Hashtag'}
onChange={e => setNewHashtag(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
</div>
</div>
<button
type="submit"
className="bg-black border text-white font-bold py-2 px-4 rounded">
Submit
</button>
<button
type="button"
onClick={clearBoards}
className="bg-black border text-white font-bold py-2 px-4 rounded mx-4">
Clear
</button>
</form>
</div>
</div>
);
};
export default Hashtags;

View File

@ -1,54 +0,0 @@
import { verifyPow } from "../../utils/mine";
import { Event } from "nostr-tools";
import NewNoteCard from "../forms/PostFormCard";
import RepostCard from "../modals/RepostCard";
import { DEFAULT_DIFFICULTY } from "../../config";
import { useUniqEvents } from "../../hooks/useUniqEvents";
import PostCard from "../modals/PostCard";
const Home = () => {
const filterDifficulty = localStorage.getItem("filterDifficulty") || DEFAULT_DIFFICULTY;
const { noteEvents, metadataEvents } = useUniqEvents();
const postEvents: Event[] = noteEvents
.filter((event) =>
verifyPow(event) >= Number(filterDifficulty) &&
event.kind !== 0 &&
(event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e" || tag[0] === "a"))
)
let sortedEvents = [...postEvents]
.sort((a, b) => {
// Sort by PoW in descending order
const powDiff = verifyPow(b) - verifyPow(a);
if (powDiff !== 0) return powDiff;
// If PoW is the same, sort by created_at in descending order
return b.created_at - a.created_at;
});
// Render the component
return (
<main className="text-white mb-20">
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
{sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={sortedEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))}
/>
:
<RepostCard
event={event}
/>
))}
</div>
</main>
);
};
export default Home;

View File

@ -1,100 +0,0 @@
import { useState, useCallback } from "react";
import PostCard from "../modals/PostCard";
import { Event } from "nostr-tools";
import RepostCard from "../modals/RepostCard";
import { useUniqEvents } from "../../hooks/useUniqEvents";
const Notifications = () => {
const [notifsView, setNotifsView] = useState(false);
const { noteEvents, metadataEvents } = useUniqEvents(undefined,true);
const storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]');
const storedPubkeys = storedKeys.map((key: any[]) => key[1]);
const postEvents = noteEvents
.filter((event) =>
event.kind !== 0 &&
storedPubkeys.includes(event.pubkey)
)
const sortedEvents = [...postEvents]
.sort((a, b) =>
b.created_at - a.created_at
)
const mentions = noteEvents
.filter((event) =>
event.kind !== 0 &&
event.tags.some((tag) => tag[0] === "p" && storedPubkeys.includes(tag[1]))
)
const sortedMentions = [...mentions]
.sort((a, b) =>
b.created_at - a.created_at
)
const toggleNotifs = useCallback(() => {
setNotifsView(prev => !prev);
}, []);
const countReplies = (event: Event) => {
return noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id));
};
// Render the component
return (
<main className="text-white mb-20">
<div className="block sm:hidden">
<label htmlFor="toggleC" className="p-4 flex items-center cursor-pointer">
<div className="relative">
<input
id="toggleC"
type="checkbox"
className="sr-only"
checked={notifsView}
onChange={toggleNotifs}
/>
<div className="block bg-gray-600 w-8 h-4 rounded-full"></div>
<div className={`dot absolute left-1 top-0.5 bg-white w-3 h-3 rounded-full transition ${notifsView ? 'transform translate-x-full bg-blue-400' : ''}`} ></div>
</div>
<div className={`ml-2 text-neutral-500 text-sm ${notifsView ? 'text-neutral-500' : ''}`}>
{notifsView ? 'Mentions' : 'Prev Posts'}
</div>
</label>
</div>
<div className="flex">
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? 'hidden sm:block' : ''}`}>
<span>Your Recent Posts</span>
{sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={countReplies(event)}
/>
:
<RepostCard
event={event}
/>
))}
</div>
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? '' : 'hidden sm:block'}`}>
<span>Mentions</span>
{sortedMentions.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={countReplies(event)}
/>
:
<RepostCard
event={event}
/>
))}
</div>
</div>
</main>
);
};
export default Notifications;

View File

@ -1,208 +0,0 @@
import React, { useState } from 'react';
import { CpuChipIcon } from '@heroicons/react/24/outline';
import { useNavigate } from 'react-router-dom';
type TestResponse = {
timeTaken: string;
hashrate: string;
};
const Settings = () => {
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 21);
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21);
const [age, setAge] = useState(localStorage.getItem('age') || 24);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [powServer, setPowServer] = useState(localStorage.getItem('powserver') || '');
const [testDiff, setTestDiff] = useState('21')
const [testResult, setTestResult] = useState<TestResponse>()
const [noteLink, setNoteLink] = useState('');
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
localStorage.setItem('filterDifficulty', String(filterDifficulty));
localStorage.setItem('difficulty', String(difficulty));
localStorage.setItem('powserver', String(powServer));
localStorage.setItem('age', String(age));
const eventData = {
difficulty: String(difficulty),
filterDifficulty: String(filterDifficulty),
powServer: String(powServer),
age: String(age),
};
const event = new CustomEvent('settingsChanged', { detail: eventData });
window.dispatchEvent(event);
};
console.log(powServer)
const handleTest = () => {
setTestResult({ timeTaken: '...', hashrate: '...' });
console.log(powServer[0])
if (powServer[0]) {
const testRequest = {
Difficulty: testDiff
};
fetch(`${powServer}/test`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testRequest)
})
.then(response => response.json())
.then(data => {
console.log(data);
// handle the response data
setTestResult(data)
})
.catch(error => {
console.error('Error:', error);
});
}
};
return (
<div className="settings-page bg-black text-white p-8 flex flex-col h-full">
<h1 className="text-lg font-semibold mb-4">Settings</h1>
<form onSubmit={handleSubmit}>
<div className="flex flex-wrap -mx-2 mb-4">
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="filterDifficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Proof-of-Work Filter:
</span>
</label>
<input
id="filterDifficulty"
type="number"
value={filterDifficulty}
onChange={e => setFilterDifficulty(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="difficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Post Difficulty {'('}<CpuChipIcon className="h-4 w-4" /> required to make post{')'}:
</span>
</label>
<input
id="difficulty"
type="number"
value={difficulty}
onChange={e => setDifficulty(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="difficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Thread Age Limit (hrs):
</span>
</label>
<input
id="age"
type="number"
value={age}
onChange={e => setAge(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
</div>
<div className='pb-4'>
<span onClick={() => setShowAdvancedSettings(!showAdvancedSettings)} className="">
{">"} Advanced Settings
</span>
{showAdvancedSettings && (
<><div className={`transition-height duration-200 ease-in-out overflow-hidden ${showAdvancedSettings ? 'h-auto' : 'h-0'} w-full md:w-1/3 px-2 mb-4 md:mb-0`}>
<label className="block text-xs mb-2" htmlFor="powServer">
Remote PoW Server:
</label>
<input
id="powServer"
type="text"
value={powServer}
onChange={e => setPowServer(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
<div className="px-2">
<label className="block text-xs mb-2" htmlFor="powServer">
Test Your PoW Server (difficulty):
</label>
<input
id="testAPI"
type="text"
value={testDiff}
onChange={e => setTestDiff(e.target.value)}
className="w-12 px-3 py-2 border rounded-md bg-black"
/>
<button type="button" onClick={handleTest} className="bg-black border text-white font-bold py-2 px-4 rounded">
Test
</button>
{testResult && (
<span>Time: {testResult.timeTaken}s with a hashrate of {testResult.hashrate}</span>
)}
</div>
</>
)}
</div>
<button
type="submit"
className="bg-black border text-white font-bold py-2 px-4 rounded">
Save Settings
</button>
</form>
<div className="settings-page pt-10">
<h1 className="text-lg font-semibold mb-4">Open Note</h1>
<form onSubmit={(e) => {e.preventDefault(); navigate(`/thread/${noteLink}`);}}>
<div className="flex flex-wrap -mx-2 mb-4">
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="filterDifficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Note ID:
</span>
</label>
<input
id="noteIDinput"
type="string"
value={noteLink}
onChange={e => setNoteLink(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
</div>
<button
type="submit"
className="bg-black border text-white font-bold py-2 px-4 rounded">
Open
</button>
</form>
</div>
<div className="settings-page py-10">
<h1 className="text-lg font-semibold mb-4">About</h1>
<div className="flex flex-col">
<p>The Anon Operation (TAO) is an anonymous-first agora, built upon the <a className="underline" href="https://nostr.com/">NOSTR protocol</a>.</p>
<br />
<p>TAO is built to facilitate unstoppable free speech on the internet.</p>
<p>-PWA to be widely accessible with distribution via URLS, and to side-step App Store gatekeeping</p>
<p>-Uses NOSTR as a censorship-resistant global "social" network</p>
<p>-Employs Proof-of-Work (PoW) as a spam prevention mechanism, as opposed to Captcha, moderation or other verification methods</p>
<br />
<a href="https://github.com/smolgrrr/TAO">
<img src="https://img.shields.io/github/stars/smolgrrr/TAO.svg?style=social" alt="Github Stars Badge" />
</a>
<div>
<span>Found a bug? dm me: <a className="underline" href="https://njump.me/npub13azv2cf3kd3xdzcwqxlgcudjg7r9nzak37usnn7h374lkpvd6rcq4k8m54">doot</a> or <a className="underline" href="mailto:smolgrrr@protonmail.com">smolgrrr@protonmail.com</a></span>
<img className="h-16" src="doot.jpeg" />
</div>
</div>
</div>
</div>
);
};
export default Settings;

View File

@ -1,195 +0,0 @@
import { useParams } from 'react-router-dom';
import { useState } from "react";
import { Event, nip19 } from "nostr-tools"
import { subNote, subNotesOnce } from '../../utils/subscriptions';
import { useEffect } from 'react';
import { uniqBy } from '../../utils/otherUtils';
import { DocumentTextIcon, FolderPlusIcon, DocumentDuplicateIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import Placeholder from '../modals/Placeholder';
import NewNoteCard from '../forms/PostFormCard';
import RepostNote from '../forms/RepostNote';
import PostCard from '../modals/PostCard';
type PostType = "" | "Reply" | "Quote" | undefined;
const Thread = () => {
const { id } = useParams();
const [events, setEvents] = useState<Event[]>([]); // Initialize state
const [OPEvent, setOPEvent] = useState<Event>()
const [showForm, setShowForm] = useState(false);
const [showRepost, setShowRepost] = useState(false);
const [postType, setPostType] = useState<PostType>("");
const [hasRun, setHasRun] = useState(false);
const [preOPEvents, setPreOPEvents] = useState(['']);
// const filterDifficulty = useState(localStorage.getItem("filterDifficulty") || "20");
// Load cached metadataEvents from localStorage
const [cachedMetadataEvents, setCachedMetadataEvents] = useState<Event[]>(
JSON.parse(localStorage.getItem("cachedMetadataEvents") || "[]")
);
let decodeResult = nip19.decode(id as string);
let hexID = decodeResult.data as string;
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
setEvents((prevEvents) => [...prevEvents, event]);
// If the new event is a metadata event, add it to the cached metadata events
if (event.kind === 0) {
setCachedMetadataEvents((prevMetadataEvents) => {
// Check if the event already exists in the cached metadata events
const existingEvent = prevMetadataEvents.find((e) => e.id === event.id || e.pubkey === event.pubkey)
if (!existingEvent) {
// If the event doesn't exist, add it to the cached metadata events
return [...prevMetadataEvents, event];
} else if (existingEvent && existingEvent.created_at < event.created_at) {
// Remove any existing metadata event with the same pubkey and id
const updatedMetadataEvents = prevMetadataEvents.filter(
(e) => e.id !== existingEvent.id
);
// Add the new metadata event
return [...updatedMetadataEvents, event];
}
// If the event already exists, return the previous cached metadata events
return prevMetadataEvents;
});
}
};
useEffect(() => {
setHasRun(false)
if (decodeResult.type === 'note') {
// Call your subNote function or do whatever you need to do with id_to_hex
subNote(hexID, onEvent);
}
}, [id]); // Empty dependency array means this useEffect runs once when the component mounts
// Save the cached metadataEvents to localStorage
useEffect(() => {
localStorage.setItem("cachedMetadataEvents", JSON.stringify(cachedMetadataEvents));
}, [cachedMetadataEvents]);
const uniqEvents = events.length > 0 ? uniqBy(events, "id") : [];
const metadataEvents = [...cachedMetadataEvents, ...uniqEvents.filter(event => event.kind === 0)];
useEffect(() => {
if (!hasRun && events.length > 0) {
let OPEvent = uniqEvents.find(event => event.id === hexID);
setOPEvent(OPEvent);
console.log(OPEvent)
if (OPEvent && OPEvent.id !== hexID) {
OPEvent = events.find(e => e.id === hexID) as Event;
}
if (OPEvent) {
setOPEvent(OPEvent);
let OPNoteEvents = OPEvent.tags.filter(tag => tag[0] === 'e').map(tag => tag[1]);
setHasRun(true);
setPreOPEvents(OPNoteEvents)
subNotesOnce(OPNoteEvents, onEvent)
}
}
}, [uniqEvents, hasRun]);
const countReplies = (event: Event) => {
return uniqEvents.filter(e => e.tags.some(tag => tag[0] === 'e' && tag[1] === event.id));
}
const repliedList = (event: Event): Event[] => {
return uniqEvents.filter(e => event.tags.some(tag => tag[0] === 'p' && tag[1] === e.pubkey));
}
const earlierEvents = uniqEvents
.filter(event =>
event.kind === 1 &&
preOPEvents.includes(event.id)
).sort((a, b) => (b.created_at as any) - (a.created_at as any));
const displayedEvents = [...uniqEvents].slice(1)
.filter(event =>
event.kind === 1 &&
!earlierEvents.map(e => e.id).includes(event.id) &&
(OPEvent ? OPEvent.id !== event.id : true)
).sort((a, b) => a.created_at - b.created_at);
if (uniqEvents.length === 0) {
return (
<>
<Placeholder />
<div className="col-span-full h-0.5 bg-neutral-900"/> {/* This is the white line separator */}
</>
);
}
return (
<>
<main className="bg-black text-white min-h-screen">
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
{earlierEvents
.filter(event => event.kind === 1)
.sort((a, b) => a.created_at - b.created_at).map((event, index) => (
<PostCard event={event} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} replies={countReplies(event)} />
))}
{OPEvent && <PostCard event={OPEvent} metadata={metadataEvents.find((e) => e.pubkey === OPEvent.pubkey && e.kind === 0) || null} replies={countReplies(OPEvent)} type={'OP'}/>}
</div>
<div className="col-span-full flex justify-center space-x-16 pb-4">
<DocumentTextIcon
className="h-5 w-5 text-gray-200 cursor-pointer"
onClick={() => {
setShowForm(prevShowForm => !prevShowForm);
setPostType('Reply');
setShowRepost(false)
}}
/>
<DocumentDuplicateIcon
className="h-5 w-5 text-gray-200 cursor-pointer"
onClick={() => {
setShowRepost(prevShowRepost => !prevShowRepost);
setShowForm(false);
}}
/>
<FolderPlusIcon
className="h-5 w-5 text-gray-200 cursor-pointer"
onClick={() => {
setShowForm(prevShowForm => !prevShowForm);
setPostType('Quote');
setShowRepost(false)
}}
/>
<a href={`nostr:${id}`} target="_blank" rel="noopener noreferrer">
<ArrowTopRightOnSquareIcon
className="h-5 w-5 text-gray-200 cursor-pointer"
/>
</a>
</div>
{(showForm && postType) &&
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<div className='text-center'>
<span >{postType}-post</span>
</div>
<NewNoteCard refEvent={OPEvent} tagType={postType}/>
</div>}
{showRepost && OPEvent && <div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<div className='text-center'>
<span>Repost note</span>
</div>
<RepostNote refEvent={OPEvent}/>
</div>}
<div className="col-span-full h-0.5 bg-neutral-900"/> {/* This is the white line separator */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{displayedEvents.map((event, index) => (
<PostCard
key={index}
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={displayedEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))}
repliedTo={repliedList(event)}
/>
))}
</div>
</main>
</>
);
};
export default Thread;

View File

@ -1,57 +0,0 @@
import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { RefObject } from "react";
import customEmojis from "../custom_emojis.json";
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
export function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = customEmojis.map((pack) => {
return {
id: pack.id,
name: pack.name,
emojis: pack.emojis
.filter((e) => !e.static_url.endsWith('.svg'))
.map((e) => {
return {
id: e.shortcode,
name: e.shortcode,
skins: [{ src: e.static_url }],
};
}),
};
});
return (
<>
<div className="absolute z-25" ref={ref}>
<Picker
autoFocus
custom={customEmojiList}
data = {data}
perLine={7}
previewPosition="none"
skinTonePosition="none"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
categories={['poast']}
/>
</div>
</>
);
}

View File

@ -1,215 +0,0 @@
import {
ServerIcon,
CpuChipIcon,
ArrowPathIcon,
PlusCircleIcon
} from "@heroicons/react/24/outline";
import { XCircleIcon } from "@heroicons/react/24/solid";
import { useState, useEffect, useRef } from "react";
import { UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { renderMedia, attachFile } from "../../utils/FileUpload";
import EmojiPicker from "@emoji-mart/react";
import customEmojis from './custom_emojis.json';
import { useSubmitForm } from "./handleSubmit";
import "../../styles/Form.css";
interface FormProps {
refEvent?: NostrEvent;
tagType?: 'Reply' | 'Quote' | '';
hashtag?: string;
}
const NewNoteCard = ({
refEvent,
tagType,
hashtag,
}: FormProps) => {
const ref = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState("");
const [file, setFile] = useState("");
const [unsigned, setUnsigned] = useState<UnsignedEvent>({
kind: 1,
tags: [
[
"client",
"getwired.app"
]
],
content: "",
created_at: Math.floor(Date.now() / 1000),
pubkey: "",
});
const [difficulty, setDifficulty] = useState(
localStorage.getItem("difficulty") || "21"
);
const [fileSizeError, setFileSizeError] = useState(false);
const [uploadingFile, setUploadingFile] = useState(false);
useEffect(() => {
if (hashtag) {
unsigned.tags.push(['t', hashtag as string]);
}
if (refEvent && tagType) {
unsigned.tags = Array.from(new Set(unsigned.tags.concat(refEvent.tags)));
unsigned.tags.push(['p', refEvent.pubkey]);
if (tagType === 'Reply') {
unsigned.tags.push(['e', refEvent.id, refEvent.tags.some(tag => tag[0] === 'e') ? 'root' : '']);
} else {
if (tagType === 'Quote') {
setComment(comment + '\nnostr:' + nip19.noteEncode(refEvent.id));
unsigned.tags.push(['q', refEvent.id]);
} else {
unsigned.tags.push(['e', refEvent.id]);
}
}
}
const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail;
setDifficulty(difficulty);
};
window.addEventListener("difficultyChanged", handleDifficultyChange);
return () => {
window.removeEventListener("difficultyChanged", handleDifficultyChange);
};
}, []);
useEffect(() => {
setUnsigned(prevUnsigned => ({
...prevUnsigned,
content: `${comment} ${file}`,
created_at: Math.floor(Date.now() / 1000),
}));
}, [comment, file]);
const { handleSubmit: originalHandleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
const handleSubmit = async (event: React.FormEvent) => {
await originalHandleSubmit(event);
setComment("");
setFile("");
setUnsigned(prevUnsigned => ({
...prevUnsigned,
content: '',
created_at: Math.floor(Date.now() / 1000)
}));
};
//Emoji stuff
const emojiRef = useRef(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
interface Emoji {
native?: string;
id?: string;
}
const emojiNames = customEmojis.map(p => p.emojis).flat();
function getEmojiById(id: string) {
return emojiNames.find(e => e.shortcode === id);
}
async function onEmojiSelect(emoji: Emoji) {
setShowEmojiPicker(false);
try {
if (emoji.id) {
const e = getEmojiById(emoji.id);
if (e) {
setComment(comment + " :" + e.shortcode + ":");
unsigned.tags.push(['emoji', e.shortcode, e.url]);
};
}
} catch {
//ignore
}
}
const topOffset = ref.current?.getBoundingClientRect().top;
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(e: React.MouseEvent) {
e.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<form
name="post"
method="post"
encType="multipart/form-data"
className=""
onSubmit={handleSubmit}
>
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={2.5 * 1024 * 1024} />
<div className="px-4 flex flex-col rounded-lg">
<textarea
name="com"
wrap="soft"
className="shadow-lg w-full px-4 py-3 border-blue-500 bg-black text-white"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={comment.split('\n').length || 1}
/>
<div className="relative">
{file !== "" && (
<button onClick={() => setFile("")}>
<XCircleIcon className="h-10 w-10 absolute shadow z-100 text-blue-500" />
</button>
)}
{renderMedia([file])}
</div>
<div className="h-14 flex items-center justify-between">
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
<div className="inline-flex items-center gap-1.5 text-neutral-300">
<CpuChipIcon className="h-4 w-4" />
</div>
<p className="text-xs font-medium text-neutral-400">
{difficulty} Work
</p>
</div>
<div>
<div className="flex items-center gap-4">
<div className="items-center">
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset || 0}
leftOffset={leftOffset || 0}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
<PlusCircleIcon className="h-4 w-4 text-neutral-400 cursor-pointer" onClick={pickEmoji} />
</div>
<button
type="submit"
className={`bg-black border h-9 inline-flex items-center justify-center px-4 rounded-lg text-white font-medium text-sm ${doingWorkProp || uploadingFile ? 'cursor-not-allowed' : ''}`}
disabled={doingWorkProp || uploadingFile}
>
Submit
</button>
</div>
</div>
</div>
</div>
{fileSizeError ? (
<span className="text-red-500">File size should not exceed 2.5MB</span>
) : null}
{doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Doing Work:</span>
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
</div>
) : null}
<div id="postFormError" className="text-red-500" />
</form>
);
};
export default NewNoteCard;

View File

@ -1,86 +0,0 @@
import {
CpuChipIcon
} from "@heroicons/react/24/outline";
import { useState, useEffect } from "react";
import { UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { useSubmitForm } from "./handleSubmit";
import "../../styles/Form.css";
interface FormProps {
refEvent: NostrEvent;
}
const RepostNote = ({
refEvent
}: FormProps) => {
const [difficulty, setDifficulty] = useState(
localStorage.getItem("difficulty") || "21"
);
const [unsigned] = useState<UnsignedEvent>({
kind: 6,
tags: [
['client', 'getwired.app'],
['e', refEvent.id, 'wss://relay.damus.io'],
['p', refEvent.pubkey]
],
content: JSON.stringify(refEvent),
created_at: Math.floor(Date.now() / 1000),
pubkey: "",
});
useEffect(() => {
const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail;
setDifficulty(difficulty);
};
window.addEventListener("difficultyChanged", handleDifficultyChange);
return () => {
window.removeEventListener("difficultyChanged", handleDifficultyChange);
};
}, []);
const { handleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
return (
<form
name="post"
method="post"
encType="multipart/form-data"
className=""
onSubmit={handleSubmit}
>
<div className="px-4 flex flex-col rounded-lg">
<div className="h-14 flex items-center justify-between">
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
<div className="inline-flex items-center gap-1.5 text-neutral-300">
<CpuChipIcon className="h-4 w-4" />
</div>
<p className="text-xs font-medium text-neutral-400">
{difficulty} PoW
</p>
</div>
<button
type="submit"
className={`bg-black border h-9 inline-flex items-center justify-center px-4 rounded-lg text-white font-medium text-sm ${doingWorkProp ? 'cursor-not-allowed' : ''}`}
disabled={doingWorkProp}
>
Submit
</button>
</div>
</div>
{doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Doing Work:</span>
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
</div>
) : null}
<div id="postFormError" className="text-red-500" />
</form>
);
};
export default RepostNote;

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +0,0 @@
import { useState, useEffect } from "react";
import { generateSecretKey, getPublicKey, finalizeEvent, UnsignedEvent } from "nostr-tools";
import { publish } from "../../utils/relays";
const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => {
const [messageFromWorker, setMessageFromWorker] = useState(null);
const [doingWorkProgress, setDoingWorkProgress] = useState(0);
const startWork = () => {
const workers = Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url)));
workers.forEach((worker, index) => {
worker.onmessage = (event) => {
if (event.data.status === 'progress') {
console.log(`Worker progress: Checked ${event.data.currentNonce} nonces.`);
setDoingWorkProgress(event.data.currentNonce);
} else if (event.data.found) {
setMessageFromWorker(event.data.event);
// Terminate all workers once a solution is found
workers.forEach(w => w.terminate());
}
};
worker.postMessage({
unsigned,
difficulty,
nonceStart: index, // Each worker starts from its index
nonceStep: numCores // Each worker increments by the total number of workers
});
});
};
return { startWork, messageFromWorker, doingWorkProgress };
};
export const useSubmitForm = (unsigned: UnsignedEvent, difficulty: string) => {
const [doingWorkProp, setDoingWorkProp] = useState(false);
const [sk, setSk] = useState(generateSecretKey());
const unsignedWithPubkey = { ...unsigned, pubkey: getPublicKey(sk) };
const powServer = useState(localStorage.getItem('powserver') || '');
const [unsignedPoWEvent, setUnsignedPoWEvent] = useState<UnsignedEvent>()
let storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]');
// Initialize the worker outside of any effects
const numCores = navigator.hardwareConcurrency || 4;
const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsignedWithPubkey, difficulty, [unsignedWithPubkey]);
useEffect(() => {
if (unsignedPoWEvent) {
setDoingWorkProp(false);
const signedEvent = finalizeEvent(unsignedPoWEvent, sk);
publish(signedEvent);
setSk(generateSecretKey())
}
}, [unsignedPoWEvent]);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setDoingWorkProp(true);
console.log(powServer[0])
if (powServer[0]) {
const inEventFormat = { ...unsignedWithPubkey, sig: "" };
const powRequest = {
req_event: inEventFormat,
difficulty: difficulty
};
fetch(`${powServer[0]}/powgen`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(powRequest)
})
.then(response => response.json())
.then(data => {
console.log(data);
// handle the response data
setUnsignedPoWEvent(data.event)
})
.catch(error => {
console.error('Error:', error);
});
} else {
startWork();
}
// Add the logic here
storedKeys.push([sk, getPublicKey(sk)]);
// Stringify the array and store it back to localStorage
localStorage.setItem('usedKeys', JSON.stringify(storedKeys));
};
useEffect(() => {
if (messageFromWorker) {
setUnsignedPoWEvent(messageFromWorker);
}
}, [messageFromWorker]);
return { handleSubmit, doingWorkProp, doingWorkProgress };
};

View File

@ -1,9 +0,0 @@
import { PropsWithChildren } from "react";
export default function CardContainer({ children }: PropsWithChildren) {
return (
<div className="card break-inside-avoid mb-4 h-min">
<div className="card-body">{children}</div>
</div>
);
}

View File

@ -1,68 +0,0 @@
// import { getLinkPreview } from 'link-preview-js';
// import { useState, useEffect } from 'react';
const LinkModal = ({ url }: { url: string }) => {
// const [linkPreview, setLinkPreview] = useState<LinkPreview | null>(null);
// const [error, setError] = useState<string | null>(null);
// const fetchWithProxy = (url: string) => {
// const proxyUrl = 'https://api.allorigins.win/raw?url=';
// return getLinkPreview(proxyUrl + url)
// .then((preview) => setLinkPreview(preview as LinkPreview))
// .catch((error) => {
// console.error("Error fetching URL with proxy:", error);
// setError('Unable to fetch URL with proxy.');
// });
// };
// useEffect(() => {
// getLinkPreview(url)
// .then((preview) => setLinkPreview(preview as LinkPreview))
// .catch(error => {
// console.error("Error fetching original URL, trying with proxy:", error);
// setError('Error fetching original URL. Trying with proxy...');
// return fetchWithProxy(url);
// });
// }, [url]);
// if (error) {
// return <a className='hover:underline text-xs text-neutral-500' href={url}>{url}</a>; // or some loading state
// }
// if (!linkPreview) {
return <a className='hover:underline text-xs text-neutral-500' href={url}>{url}</a>; // or some loading state
// }
// return (
// <div className="link-preview p-1 bg-neutral-800 rounded-lg border border-neutral-800">
// <a href={linkPreview.url} target="_blank" rel="noopener noreferrer" className="">
// <img src={linkPreview.images[0]} alt={linkPreview.title} className="rounded-lg" />
// <div className="font-semibold text-xs text-gray-300">
// {linkPreview.title}
// </div>
// </a>
// </div>
// );
};
// interface LinkPreview {
// url: string;
// title: string;
// siteName?: string;
// description?: string;
// mediaType: string;
// contentType?: string;
// images: string[];
// videos: {
// url?: string;
// secureUrl?: string;
// type?: string;
// width?: string;
// height?: string;
// [key: string]: any;
// }[];
// [key: string]: any;
// }
export default LinkModal;

View File

@ -1,61 +0,0 @@
import { parseContent } from "../../../utils/content";
import { Event } from "nostr-tools";
import { getMetadata } from "../../../utils/getMetadata";
import ContentPreview from "./TextModal";
import { renderMedia } from "../../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../../utils/cardUtils";
import { CpuChipIcon } from "@heroicons/react/24/outline";
import { verifyPow } from "../../../utils/mine";
const QuoteEmbed = ({
key,
event,
metadata,
}: {
key?: string | number;
event: Event;
metadata: Event | null;
}) => {
const { files } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
let metadataParsed = null;
if (metadata !== null) {
metadataParsed = getMetadata(metadata);
}
return (
<div className="p-3 rounded-lg border border-neutral-700">
<div className="flex flex-col gap-2">
<div className="flex flex-col break-words">
<ContentPreview key={event.id} eventdata={event} />
</div>
<div className="flex justify-between items-center">
{metadataParsed ?
<img
key={key}
className={`h-5 w-5 rounded-full`}
src={metadataParsed?.picture ?? icon}
alt=""
loading="lazy"
decoding="async" />
:
<div className={`h-4 w-4 ${icon} rounded-full`} />
}
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(event)}
</div>
<span className="text-neutral-700">·</span>
<div className="text-xs font-semibold text-neutral-600">
{timeAgo(event.created_at)}
</div>
<span className="text-neutral-700">·</span>
</div>
</div>
</div>
</div>
);
};
export default QuoteEmbed;

View File

@ -1,102 +0,0 @@
import React from "react";
import { Event } from "nostr-tools";
import { useEffect, useState } from "react";
import { subNoteOnce } from "../../../utils/subscriptions";
import { nip19 } from "nostr-tools";
import { parseContent } from "../../../utils/content";
import QuoteEmbed from "./QuoteEmbed";
import LinkModal from "./LinkPreview";
const RichText = ({ text, isExpanded, emojiMap }: { text: string; isExpanded: boolean; emojiMap: Record<string, any> }) => {
const content = isExpanded ? text.split('\n') : text.slice(0, 350).split('\n');
return (
<>
{content.map((line, i) => (
<div key={i}>
{line.split(' ').map((word, j) =>
emojiMap[word]
? <img className="w-8 h-8 mx-0.5 inline" src={emojiMap[word]} alt={word} key={j} />
: `${word} `
)}
</div>
))}
</>
);
};
const ContentPreview = ({ key, eventdata }: { key: string; eventdata: Event }) => {
const { comment } = parseContent(eventdata);
const [finalComment, setFinalComment] = useState(comment);
const [quoteEvents, setQuoteEvents] = useState<Event[]>([]); // Initialize state
const [isExpanded, setIsExpanded] = useState(false);
const [url, setUrl] = useState("");
const [emojiMap, setEmojiMap] = useState<Record<string, any>>({});
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
setQuoteEvents((prevEvents) => [...prevEvents, event]);
};
useEffect(() => {
const findUrl = comment.match(/\bhttps?:\/\/\S+/gi);
if (findUrl && findUrl.length > 0) {
setUrl(findUrl[0]);
setFinalComment(finalComment.replace(findUrl[0], "").trim());
}
const match = comment.match(/\bnostr:([a-z0-9]+)/i);
const nostrQuoteID = match && match[1];
if (nostrQuoteID && nostrQuoteID.length > 0) {
let id_to_hex = String(nip19.decode(nostrQuoteID as string).data);
subNoteOnce(id_to_hex, onEvent);
setFinalComment(finalComment.replace("nostr:" + nostrQuoteID, "").trim());
}
let newEmojiMap: Record<string, any> = {};
eventdata.tags.forEach(tag => {
if (tag[0] === "emoji") {
newEmojiMap[`:${tag[1]}:`] = tag[2];
}
});
// Update the state variable
setEmojiMap(newEmojiMap);
}, [comment, finalComment]);
const getMetadataEvent = (event: Event) => {
const metadataEvent = quoteEvents.find(
(e) => e.pubkey === event.pubkey && e.kind === 0
);
if (metadataEvent) {
return metadataEvent;
}
return null;
};
return (
<div className="gap-2 flex flex-col break-words text-xs">
<RichText text={finalComment} isExpanded={isExpanded} emojiMap={emojiMap} />
{finalComment.length > 350 && (
<button
className="text-sm text-neutral-500"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? "...Read less" : "...Read more"}
</button>
)}
{url !== "" && <LinkModal key={key} url={url} />}
{quoteEvents[0] && quoteEvents.length > 0 && (
<a href={`/thread/${nip19.noteEncode(quoteEvents[0].id)}`}>
<QuoteEmbed
key={key}
event={quoteEvents[0]}
metadata={getMetadataEvent(quoteEvents[0])}
/>
</a>
)}
</div>
);
};
export default ContentPreview;

View File

@ -1,95 +0,0 @@
import React, { useState, useEffect } from 'react';
import { XMarkIcon } from "@heroicons/react/24/solid";
import { ArrowUpOnSquareIcon, PlusCircleIcon } from '@heroicons/react/24/outline';
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
declare global {
interface Navigator {
standalone?: boolean;
}
}
const AddToHomeScreenPrompt: React.FC = () => {
const [inMobileBrowser, setInMobileBrowser] = useState(false);
useEffect(() => {
const checkPWA = () => {
// Check if the app is running as a PWA on Android
const isAndroidPWA = window.matchMedia('(display-mode: standalone)').matches ||
window.matchMedia('(display-mode: minimal-ui)').matches;
// Check if the app is running as a PWA on other platforms
const isOtherPWA = window.navigator.standalone;
return !isAndroidPWA && !isOtherPWA;
};
// Function to detect mobile browser
const detectMobileBrowser = () => {
return (
(navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/Windows Phone/i))
);
};
const timer = setTimeout(() => {
setInMobileBrowser(Boolean(checkPWA() && detectMobileBrowser()));
}, 2000); // 3000 milliseconds = 3 seconds
// Cleanup function to clear the timeout if the component unmounts before the timeout finishes
return () => clearTimeout(timer);
}, []);
if (!inMobileBrowser) {
return null;
}
return (
<Transition appear show={inMobileBrowser} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={() => setInMobileBrowser(false)}
>
<div className="min-h-screen px-4 text-center">
<Dialog.Overlay className="fixed inset-0 bg-gray-800 opacity-40" />
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div className="fixed bottom-0 left-0 right-0 p-4 bg-neutral-900 rounded-lg m-2 border border-neutral-700 shadow-md flex justify-between items-center animate-slide-up">
<div className="flex flex-col text-white">
<span className="font-semibold">Stay Wired</span>
<p className="text-xs">Add Wired to your home screen for a better experience</p>
<ul className="list-none mt-2 text-sm">
<li>
<div className="flex items-center">
<span className="mr-2">{'>'}</span> Click on <ArrowUpOnSquareIcon className="h-6 w-6 ml-1 text-blue-500" /> <span className="font-semibold text-blue-500">Share</span>
</div>
</li>
<li>
<div className="flex items-center">
<span className="mr-2">{'>'}</span> Click <PlusCircleIcon className="h-6 w-6 ml-1 text-blue-500" /> <span className="font-semibold text-blue-500">Add to Home Screen</span>
</div>
</li>
</ul>
</div>
<button className="absolute top-2 right-2" onClick={() => {setInMobileBrowser(!inMobileBrowser);}}>
<XMarkIcon className="h-6 w-6 text-white" />
</button>
</div>
</div>
</Dialog>
</Transition>
);
};
export default AddToHomeScreenPrompt;

View File

@ -1,54 +0,0 @@
import {
Cog6ToothIcon,
BellIcon,
HashtagIcon
} from "@heroicons/react/24/outline";
import { useLocation } from "react-router-dom";
export default function Header() {
const location = useLocation();
const pathParts = location.pathname.split('/');
const secondLastPart = pathParts[pathParts.length - 2];
const lastPathPart = secondLastPart === "thread" ? "/thread" : "/" + pathParts[pathParts.length - 1];
return (
<header className="mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div className="flex justify-between items-center">
<a href="/">
<div className="flex items-center gap-2">
<img src="/icon.png" className="h-12" alt="logo" />
<span className="font-semibold text-white">
{`~/WIRED${lastPathPart}>`}
</span>
</div>
</a>
<div>
<a
href="/hashtags"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<HashtagIcon className="h-5 w-5" />
</button>
</a>
<a
href="/notifications"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<BellIcon className="h-5 w-5" />
</button>
</a>
<a
href="/settings"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<Cog6ToothIcon className="h-5 w-5" />
</button>
</a>
</div>
</div>
</header>
);
}

View File

@ -1,24 +0,0 @@
const Placeholder = () => {
return (
<div className="mx-auto gap-4 p-4">
<div className="border border-blue-300 shadow rounded-md p-4 max-w-sm w-full mx-auto">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-slate-700 h-10 w-10"></div>
<div className="flex-1 space-y-6 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
</div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Placeholder;

View File

@ -1,119 +0,0 @@
// import CardContainer from "./CardContainer";
import { CpuChipIcon } from "@heroicons/react/24/outline";
// import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools";
import { getMetadata, Metadata } from "../../utils/getMetadata";
// import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../utils/cardUtils";
import { verifyPow } from "../../utils/mine";
import { useNavigate } from 'react-router-dom';
import { subNoteOnce } from "../../utils/subscriptions";
import { useEffect, useState } from "react";
import ContentPreview from "./CardModals/TextModal";
interface RepostProps {
key?: string | number;
event: Event;
}
const RepostCard = ({
key,
event
}: RepostProps) => {
const repostedEvent = JSON.parse(event.content);
// const { files } = parseContent(repostedEvent);
const icon = getIconFromHash(event.pubkey);
const navigate = useNavigate();
const [cachedMetadataEvents, setCachedMetadataEvents] = useState<Event[]>(
JSON.parse(localStorage.getItem("cachedMetadataEvents") || "[]")
);
const [metadata, setMetadata] = useState<Metadata>()
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
const existingEvent = cachedMetadataEvents.find((e) => e.pubkey === event.pubkey)
if (existingEvent) {
setMetadata(getMetadata(existingEvent))
}
else if (!existingEvent && event.kind === 0 && event.pubkey === repostedEvent.pubkey && metadata == null) {
setMetadata(getMetadata(event))
setCachedMetadataEvents((prevMetadataEvents) => {
// Check if the event already exists in the cached metadata events
const existingEvent = prevMetadataEvents.find((e) => e.id === event.id || e.pubkey === event.pubkey)
if (!existingEvent) {
// If the event doesn't exist, add it to the cached metadata events
return [...prevMetadataEvents, event];
} else if (existingEvent && existingEvent.created_at < event.created_at) {
// Remove any existing metadata event with the same pubkey and id
const updatedMetadataEvents = prevMetadataEvents.filter(
(e) => e.id !== existingEvent.id
);
// Add the new metadata event
return [...updatedMetadataEvents, event];
}
// If the event already exists, return the previous cached metadata events
return prevMetadataEvents;
});
}
};
useEffect(() => {
subNoteOnce(repostedEvent.id, onEvent);
}, [repostedEvent.id]);
// Save the cached metadataEvents to localStorage
useEffect(() => {
localStorage.setItem("cachedMetadataEvents", JSON.stringify(cachedMetadataEvents));
}, [cachedMetadataEvents]);
const handleClick = () => {
navigate(`/thread/${nip19.noteEncode(repostedEvent.id)}`);
};
return (
<div>
<div className="ml-1 flex text-sm text-neutral-600 gap-2.5">
Repost
@
<span className="inline-flex"><CpuChipIcon className="h-5 w-5" /> {verifyPow(event)}</span>
</div>
<div className="rounded-lg border border-neutral-700">
<div className="card break-inside-avoid h-min">
<div className="card-body">
<div className={`flex flex-col gap-2`}>
<div className={`flex flex-col break-words hover:cursor-pointer`} onClick={handleClick}>
<ContentPreview key={repostedEvent.id} eventdata={repostedEvent} />
</div>
<div className={`flex justify-between items-center hover:cursor-pointer`} onClick={handleClick}>
{metadata ?
<img
key = {key}
className={`h-5 w-5 rounded-full`}
src={metadata?.picture ?? icon}
alt=""
loading="lazy"
decoding="async"/>
:
<div className={`h-4 w-4 ${icon} rounded-full`} />
}
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(repostedEvent)}
</div>
<span className="text-neutral-700">·</span>
<div className="text-xs font-semibold text-neutral-600">
{timeAgo(repostedEvent.created_at)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default RepostCard;