diff --git a/app/t/[txid]/page.tsx b/app/t/[txid]/page.tsx index 3cdfee4..fa7a774 100644 --- a/app/t/[txid]/page.tsx +++ b/app/t/[txid]/page.tsx @@ -43,6 +43,38 @@ type Props = { params: { txid: string } } +const playerKeys = [ + "youtube", + "youtu", + "soundcloud", + "facebook", + "vimeo", + "wistia", + "mixcloud", + "dailymotion", + "twitch", +]; + +function extractUrls(text: string) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + return text.match(urlRegex); + } + +function normalizeUrls(urls: string[]): string[] { + const normalizedUrls: string[] = []; + + for (const url of urls) { + let normalizedUrl = url; + + // Remove the "m." subdomain from the URL + normalizedUrl = normalizedUrl.replace(/m\./i, ""); + + normalizedUrls.push(normalizedUrl); + } + + return normalizedUrls; +} + interface Player { id?: number, name?: string; @@ -55,6 +87,7 @@ interface Player { interface BitcoinFile { contentType: string; content: string; + txid?: string; } interface BoostTag { @@ -75,32 +108,53 @@ export interface TransactionDetails { textContent?: string; files?: BitcoinFile[]; urls?: URLPreview[]; + playableURLs?: string[]; + tweetId?: string; difficulty?: number; tags?: BoostTag[]; inReplyToTx?: string; replies?: TransactionDetails[]; app?: string; createdAt?: Date; + json?: any; + smartContractClass?: string; } async function getTransactionDetails(txid: string): Promise { - const [twetchResult, contentResponse, repliesResponse, onchainData] = await Promise.all([ + + const [twetchResult, contentResponse, repliesResponse, onchainData, issueResponse, videoResponse] = await Promise.all([ twetchDetailQuery(txid).catch((err) => console.log(err)), - axios.get(`https://pow.co/api/v1/content/${txid}`), - axios.get(`https://pow.co/api/v1/content/${txid}/replies`), - axios.get(`https://onchain.sv/api/v1/events/${txid}`) + axios.get(`https://pow.co/api/v1/content/${txid}`).catch((err) => { + return { data: {}} + }), + axios.get(`https://pow.co/api/v1/content/${txid}/replies`).catch((err) => { + return { data: {}} + }), + axios.get(`https://onchain.sv/api/v1/events/${txid}`).catch((err) => { + return { data: {}} + }), + axios.get(`https://pow.co/api/v1/issues/${txid}`).catch((err) => { + return { data: null} + }), + axios.get(`https://hls.pow.co/api/v1/videos/${txid}`).catch((err) =>{ + return { data: null} + }) ]) - let { content } = contentResponse.data + let { content } = contentResponse.data || {} + let issue = issueResponse.data + let video = issueResponse.data let { tags } = contentResponse.data let { events } = onchainData.data; - let difficulty = tags.reduce((acc: number, curr: any) => acc + curr.difficulty, 0) + let difficulty = tags?.reduce((acc: number, curr: any) => acc + curr.difficulty, 0) || 0 let replies = repliesResponse.data.replies || [] let inReplyToTx = contentResponse.data.context_txid || null - let author, textContent, app, createdAt; + let author, textContent, app, createdAt, json, smartContractClass, tweetId; let urls: string[] = [] - let files = [] + let playableURLs: string[] = [] + let files: BitcoinFile[] = [] + let previewURLs: URLPreview[] = [] if (twetchResult){ textContent = twetchResult.bContent @@ -109,27 +163,56 @@ async function getTransactionDetails(txid: string): Promise { + twetchResult.files && JSON.parse(twetchResult.files).map(async (fileTx:string) => { let src = `https://dogefiles.twetch.app/${fileTx}` let response = await fetch(src) let mime = response.headers.get("content-type") - return { - contentType: mime, - content: src + files.push({ + contentType: mime!, + content: src, + txid: fileTx - } + }) }) urls = parseURLsFromMarkdown(textContent) + for (const url of urls){ + const preview = await fetchPreview(url) + previewURLs.push(preview) + } inReplyToTx = twetchResult.postByReplyPostId?.transaction - replies = twetchResult.postsByReplyPostId?.edges?.map((node:any) => node.transaction) + twetchResult.postsByReplyPostId?.edges?.map((node:any) => { + replies.push({txid: node.node.transaction}) + }) app = "twetch.com" createdAt = twetchResult.createdAt - } - if (content.bmap){ - content.bmap.B.forEach((bContent: any) => { + } else if (issue) { + smartContractClass = 'issue' + json = issue + createdAt = issue.origin.createdAt + } else if (content.content_type?.includes("calendar")){ + json = content.content_json + smartContractClass = 'calendar' + createdAt = content.createdAt + } else if (video) { + try { + await axios.head(`https://hls.pow.co/${video.sha256Hash}.m3u8`) + playableURLs.push(`https://hls.pow.co/${video.sha256Hash}.m3u8`) + } catch (error) { + playableURLs.push(`https://hls.pow.co/${video.sha256Hash}.mp4`) + } + } else if (content.bmap && content.bmap.B && content.bmap.MAP[0]){ + //content.bmap.B.forEach(async (bContent: any) => { + for (const bContent of content.bmap.B){ if (bContent['content-type'].includes("text")){ textContent = bContent.content + urls = extractUrls(textContent) || [] urls = parseURLsFromMarkdown(textContent) + urls = normalizeUrls(urls); + console.log("URLS",urls) + for (const url of urls){ + const preview = await fetchPreview(url) + previewURLs.push(preview) + } } else if (bContent["content-type"].includes("image")) { files.push({ contentType: bContent["content-type"], @@ -138,7 +221,7 @@ async function getTransactionDetails(txid: string): Promise { - if(evt.type === "url"){ - urls.push(evt.content.url) + } else if (events){ + createdAt = events[0].createdAt + for (const ev of events){ + if (ev.type === "url") { + if (ev.content.url.match(/.m3u8$/)) { + console.log("PLAYABLE URL", ev.content.url) + playableURLs.push(ev.content.url as string) + } else if (playerKeys.some((key) => ev.content.url.includes(key))) { + let normalizedUrls = normalizeUrls([ev.content.url]); + normalizedUrls.forEach(url => { + playableURLs.push(url) + }); + } else if (ev.content.url.includes("twitter")) { + tweetId = ev.content.url.split("/").pop(); + } else { + const preview = await fetchPreview(ev.content.url) + previewURLs.push(preview); + } } - }); + }; } - let previewUrls - console.log(urls) - /* if(urls?.length > 0){ - urls?.forEach(async url => { - - let preview = await fetchPreview(url) - previewUrls?.push(preview) - }) - - } */ const txDetails: TransactionDetails = { txid, author, textContent, files, - urls: previewUrls, + urls: previewURLs, + playableURLs, + tweetId, difficulty, tags, inReplyToTx, replies, app, - createdAt - + createdAt, + json, + smartContractClass } return txDetails @@ -247,12 +336,14 @@ const TransactionDetailPage = async ({ }: { params: { txid: string } }) => { + const details = await getTransactionDetails(txid) + return (
{details?.inReplyToTx && } - + {} {details?.replies?.map((reply) => )}
diff --git a/components/BoostContentCard.tsx b/components/BoostContentCard.tsx index fc7dc24..92e39b0 100644 --- a/components/BoostContentCard.tsx +++ b/components/BoostContentCard.tsx @@ -256,7 +256,7 @@ function BoostContentCard({ const navigate = (e: any) => { e.stopPropagation(); - router.push(`/${content_txid}`); + router.push(`/t/${content_txid}`); }; if (!content) { diff --git a/components/BoostContentCardV2.tsx b/components/BoostContentCardV2.tsx index b83b326..c466836 100644 --- a/components/BoostContentCardV2.tsx +++ b/components/BoostContentCardV2.tsx @@ -456,7 +456,7 @@ const BoostContentCardV2 = ({ const navigate = (e: any) => { e.stopPropagation(); - router.push(`/${content_txid}`); + router.push(`/t/${content_txid}`); }; return ( diff --git a/components/FindOrCreate.tsx b/components/FindOrCreate.tsx index 409d5eb..e7fb1d2 100644 --- a/components/FindOrCreate.tsx +++ b/components/FindOrCreate.tsx @@ -70,8 +70,8 @@ function FindOrCreate() { }); console.log(tx); - router.prefetch(`/${tx.hash}`); - router.push(`/${tx.hash}`); + router.prefetch(`/t/${tx.hash}`); + router.push(`/t/${tx.hash}`); } catch (error) { console.log(error); toast("Error!", { diff --git a/components/IssueCard.tsx b/components/IssueCard.tsx index e61a4c1..84ba709 100644 --- a/components/IssueCard.tsx +++ b/components/IssueCard.tsx @@ -1,28 +1,22 @@ -import React, { useEffect, useState } from 'react'; -import bsv from 'bsv'; -import { Issue } from '../src/contracts/issue'; -import { toast } from 'react-hot-toast'; -import useWallet from '../hooks/useWallet'; -import { addBounty, addComment, assignIssue, completeIssue } from '../services/issues'; -import { useRouter } from 'next/router'; -import { TxOutputRef } from 'scrypt-ts'; -import axios from 'axios'; -import { parse } from 'path'; +import { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import { IssueOperator } from "../services/issue_operator"; +import useWallet from "../hooks/useWallet"; - -export interface Signer { - // Define the properties of the Signer interface as per your requirement -} - - -export interface NewIssue { - signer: Signer; - organization: string; - repo: string; +interface IssueCardProps { + methodCalls: any[]; title: string; description: string; - owner: bsv.PublicKey; - assignee: bsv.PublicKey; + repo: string; + organization: string; + owner: string; + assignee: string; + closed: boolean; + completed: boolean; + origin: string; + location: string; + txid: string; } interface Comment { @@ -30,179 +24,76 @@ interface Comment { comment: string; } -interface IssueCardProps { - issue: Issue; - onAddBounty: (issue: Issue) => Promise; - onLeaveComment: (issue: Issue) => Promise; - onMarkAsComplete: (issue: Issue) => Promise; - refresh: () => void; - origin: any; - methodCalls: any[]; -} - -const IssueCard: React.FC = (props: { - issue: Issue, - onAddBounty: (issue: Issue) => Promise, - onLeaveComment: (issue: Issue) => Promise, - onMarkAsComplete: (issue: Issue) => Promise, - refresh: () => void, - origin: any, - methodCalls: any[], -}) => { - const [isOwner, setIsOwner] = useState(false); // You can set this based on the user's public key - +const IssueCard = (issue: IssueCardProps) => { + const router = useRouter() + const [contractOperator, setContractOperator] = useState(null) + const wallet = useWallet() const [addingBounty, setAddingBounty] = useState(false); const [satoshis, setSatoshis] = useState(null); - const [newBounty, setNewBounty] = useState(BigInt(props.issue.balance -1)); - const [issue, setIssue] = useState(props.issue); - const [location, setLocation] = useState((props.issue.from as TxOutputRef)?.tx?.hash); - const [origin, setOrigin] = useState(null); + const [newBounty, setNewBounty] = useState(0n); + const [assignPopupVisible, setAssignPopupVisible] = useState(false); + const isOwner = useMemo(() => wallet?.publicKey!.toString() === issue.owner, [wallet]) + const [assigning, setAssigning] = useState(false); + const [assignSuccess, setAssignSuccess] = useState(false); + const [publicKeyInput, setPublicKeyInput] = useState(''); const [completionStatus, setCompletionStatus] = useState<'incomplete' | 'posting' | 'complete'>('incomplete'); const [commentBoxVisible, setCommentBoxVisible] = useState(false); const [newComment, setNewComment] = useState(''); const [comments, setComments] = useState([]); - const [assigning, setAssigning] = useState(false); - const [assignSuccess, setAssignSuccess] = useState(false); - const [assignPopupVisible, setAssignPopupVisible] = useState(false); - const [publicKeyInput, setPublicKeyInput] = useState(''); - - const handleAssignButtonClick = () => { - setAssignPopupVisible(true); - }; - - const handleAssignSubmit = async () => { - setAssigning(true); - try { - const assignee = new bsv.PublicKey(publicKeyInput); - - if (!wallet) { return } - - toast(`Assigning issue to ${publicKeyInput}...`, { - icon: '⛏️', - style: { - borderRadius: '10px', - background: '#333', - color: '#fff', - }, - }); - - const [newIssue, tx] = await assignIssue({ - issue, - assignee, - signer: wallet.signer, - }); - - - const result = await axios.get(`https://pow.co/api/v1/issues/${(newIssue.from as TxOutputRef)?.tx?.hash}`); - - toast(`Issue re-assigned`, { - icon: '⛏️', - style: { - borderRadius: '10px', - background: '#333', - color: '#fff', - }, - }); - console.log({ result }) - setIssue(newIssue); - setNewBounty(BigInt(newIssue.balance - 1)); - props.refresh(); - - setAssignSuccess(true); - setAssigning(false); - setAssignPopupVisible(false); - - } catch (error) { - - console.error('Invalid public key', error); - - toast.error('Invalid public key or other error re-assigning'); - setAssigning(false); - } + const parseComments = (json: any) => { + const _comments = json.methodCalls + .filter((call: any) => call.method === 'addComment') + .map((commentCall: any) => { + console.log('commentCall', commentCall) + return { + comment: Buffer.from(commentCall.arguments.find((arg: any) => arg.name === 'comment').value, 'hex').toString('utf8'), + commenter: commentCall.arguments.find((arg: any) => arg.name === 'commenter').value, + } + }) + + console.log(_comments) + + setComments(_comments) + }; -const parseComments = (json: any) => { - const _comments = json.methodCalls - .filter((call: any) => call.method === 'addComment') - .map((commentCall: any) => { - console.log('commentCall', commentCall) - return { - comment: Buffer.from(commentCall.arguments.find((arg: any) => arg.name === 'comment').value, 'hex').toString('utf8'), - commenter: commentCall.arguments.find((arg: any) => arg.name === 'commenter').value, - } - }) - - console.log(_comments) + useEffect(() => { + parseComments({methodCalls: issue.methodCalls}) + },[]) - setComments(_comments) -}; -const {methodCalls} = props; + useEffect(() => { + IssueOperator.load({origin: issue.origin, signer: wallet!.signer }).then(setContractOperator) + },[wallet]) -useEffect(() => { - parseComments(props) -}, [methodCalls]) + useEffect(() => { + contractOperator && console.log("contract operator loaded", contractOperator) +},[]) - const router = useRouter(); + const handleAddBounty = (e:any) => { + e.preventDefault() + addBounty() + } + const addBounty = async() => { - const wallet = useWallet() + } const handleAddBountyClick = () => { setAddingBounty(true); }; - useEffect(() => { - - setIsOwner(wallet?.publicKey?.toString() === issue.owner?.toString()); - - }, [issue]); - - const handleConfirmClick = async () => { - if (!wallet) return; - setAddingBounty(false); - const addedBounty = BigInt(Number(satoshis)); - - toast(`Adding ${satoshis} satoshis to the bounty...`, { - icon: '⛏️', - style: { - borderRadius: '10px', - background: '#333', - color: '#fff', - }, - }); - - await issue.connect(wallet.signer) - - console.log('signer', wallet.signer) + const handleAssign = (e:any) => { + e.preventDefault() + assign() + } - const [newIssue, tx] = await addBounty({ - satoshis: addedBounty, - signer: wallet.signer, - issue, - }); + const assign = async () => { - - - const result = await axios.get(`https://pow.co/api/v1/issues/${(newIssue.from as TxOutputRef)?.tx?.hash}`); - console.log({ result }) - setIssue(newIssue); - setNewBounty(BigInt(newIssue.balance - 1)); - props.refresh(); + } - }; - - const handleMarkAsComplete = async () => { - if (!wallet) return - setCompletionStatus('posting'); - // Simulate async operation like posting a transaction - // eslint-disable-next-line no-promise-executor-return - const [newIssue, tx] = await completeIssue({ issue, signer: wallet.signer }); - const { data } = await axios.get(`https://pow.co/api/v1/issues/${(newIssue.from as TxOutputRef)?.tx?.hash}`); - console.log('complete', data) - setIssue(newIssue) - props.refresh(); - setCompletionStatus('complete'); + const handleAssignButtonClick = () => { + setAssignPopupVisible(true); }; const handleAddCommentClick = () => { @@ -213,147 +104,214 @@ useEffect(() => { setNewComment(e.target.value); }; - const handlePostCommentClick = async () => { + const handleComment = (e:any) => { + e.preventDefault() + comment() + } - if (!wallet) return; - try { - toast.success('Posting new comment'); + const comment = async() => { - const [newIssue, tx] = await addComment({ - issue, - comment: newComment, - signer: wallet.signer - }); + } - - - const result = await axios.get(`https://pow.co/api/v1/issues/${tx.hash}`); - console.log({ result }) - setIssue(newIssue); - setNewBounty(BigInt(newIssue.balance - 1)); - props.refresh(); + const handleComplete = (e:any) => { + e.preventDefault() + complete() + } - // Assuming onLeaveComment is a function that posts the comment and returns the JSON response - //const json = await onLeaveComment(issue, newComment); - //parseComments(json); - setNewComment(''); - setCommentBoxVisible(false); - toast.success('Comment posted successfully'); - } catch (error) { - console.error(error) - toast.error('Failed to post the comment'); - } - }; + const complete = async () => { - const title = Buffer.from(issue.title, 'hex').toString('utf8'); - const description = Buffer.from(issue.description, 'hex').toString('utf8'); - const organization = Buffer.from(issue.organization, 'hex').toString('utf8'); - const repo = Buffer.from(issue.repo, 'hex').toString('utf8'); + } - console.log('ORIGIN', props.origin) + const navigate = (e:any) => { + e.preventDefault() + e.stopPropagation() + router.push(`/issues/${issue.origin}`) + } return ( -
-

{title}

-

{organization}/{repo}

-

{description}

-

Bounty: {newBounty.toString()}

-

Location: {props.origin.location}

-

Origin: {props.origin.origin}

- -
- - {addingBounty ? ( -
- setSatoshis(Number(e.target.value))} className="border p-2 rounded" placeholder="Enter Satoshis" /> - -
- ) : ( - (!issue.closed && ( - - )) - - )} - - {isOwner && !issue.closed && ( - <> - - {assignPopupVisible && ( -
-
- - setPublicKeyInput(e.target.value)} className="mt-1 p-2 w-full border rounded-md"/> - -
+
+ +

+ {issue.title} +

+ +
+

+ {issue.organization}/{issue.repo} +

+

{issue.description}

+

+ Bounty:{" "} + + {newBounty.toString()} sats + +

+

+ Location:{" "} + + {issue.location} + +

+

+ Origin:{" "} + + {issue.origin} + +

+ +
+ {addingBounty ? ( +
+ setSatoshis(Number(e.target.value))} + className="border p-2 rounded" + placeholder="Enter Satoshis" + /> +
+ ) : ( + !issue.closed && ( + + ) )} - - )} - + {assignPopupVisible && ( +
+
+ + setPublicKeyInput(e.target.value)} + className="mt-1 p-2 w-full border rounded-md" + /> + +
+
+ )} + + )} + + {commentBoxVisible && (
-