mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 01:11:25 +00:00
should fix
This commit is contained in:
parent
85ba016ef7
commit
6f7d41edd0
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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
@ -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 };
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"
|
|
||||||
>
|
|
||||||
​
|
|
||||||
</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;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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
@ -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 };
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"
|
|
||||||
>
|
|
||||||
​
|
|
||||||
</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;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
Loading…
Reference in New Issue
Block a user