Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crosspost replies to nostr #1093

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions components/item-info.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'
import { ITEM_FULL } from '@/fragments/items'
import Badge from 'react-bootstrap/Badge'
import Dropdown from 'react-bootstrap/Dropdown'
import Countdown from './countdown'
Expand Down Expand Up @@ -33,10 +35,19 @@ export default function ItemInfo ({
const [canEdit, setCanEdit] =
useState(item.mine && (Date.now() < editThreshold))
const [hasNewComments, setHasNewComments] = useState(false)
const [isParentCrossposted, setIsParentCrossposted] = useState(false)
const [meTotalSats, setMeTotalSats] = useState(0)
const root = useRoot()
const sub = item?.sub || root?.sub

const { data } = useQuery(ITEM_FULL, { variables: { id: item?.parentId } })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The performance implications of this are significant. For every comment, it queries the server for the entire parent. In a comment thread with 100 comments, this will make 100 requests to the server.

I think what we want to do instead is when rendering a Comment component from a parent item/comment, send it this info as props.

When a comment is in list of comments like on a profile, you'll want to use a different query fragment with a nested resolver for parent so that this data is gathered when all the other data is. An example of this is the nested resolver for root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet thanks for the feedback! I'll get right on this


useEffect(() => {
if (data && data.item && data.item.noteId) {
setIsParentCrossposted(true)
}
}, [data])

useEffect(() => {
if (!full) {
setHasNewComments(newComments(item))
Expand Down Expand Up @@ -164,8 +175,12 @@ export default function ItemInfo ({
nostr note
</Dropdown.Item>
)}
{/* for crosspost top level items */}
{item && item.mine && !item.noteId && !item.isJob && !item.parentId &&
<CrosspostDropdownItem item={item} />}
{/* for crosspost replies */}
{root && item && item.mine && !item.noteId && root.noteId && isParentCrossposted &&
<CrosspostDropdownItem item={item} />}
{me && !item.position &&
!item.mine && !item.deletedAt &&
(item.meDontLikeSats > meTotalSats
Expand Down
76 changes: 61 additions & 15 deletions components/use-crossposter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DEFAULT_CROSSPOSTING_RELAYS, crosspost, callWithTimeout } from '@/lib/n
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
import { SETTINGS } from '@/fragments/users'
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
import { nip19 } from 'nostr-tools'

async function discussionToEvent (item) {
const createdAt = Math.floor(Date.now() / 1000)
Expand Down Expand Up @@ -72,6 +73,48 @@ async function bountyToEvent (item) {
}
}

async function replyToEvent (item, fetchItemData) {
const createdAt = Math.floor(Date.now() / 1000)
const rootId = item.root?.id

if (!rootId) {
throw new Error('Failed to get parent item')
}

const replyToItem = await fetchItemData(rootId)
if (!replyToItem) {
throw new Error('Failed to get parent item')
}

const replyTo = replyToItem.noteId

if (!replyTo) {
throw new Error('Failed to get parent item')
}

if (replyTo.includes('naddr')) {
const { kind, pubkey, identifier } = nip19.decode(replyTo)?.data || {}

if (!kind || !pubkey || !identifier) {
throw new Error('Invalid naddr')
}

return {
created_at: createdAt,
kind: 1,
content: item.text,
tags: [['a', `${kind}:${pubkey}:${identifier}`]]
}
} else {
return {
created_at: createdAt,
kind: 1,
content: item.text,
tags: [['e', replyTo]]
}
}
}

export default function useCrossposter () {
const toaster = useToast()
const { data } = useQuery(SETTINGS)
Expand Down Expand Up @@ -102,6 +145,17 @@ export default function useCrossposter () {
}`
)

const fetchItemData = async (itemId) => {
try {
const { data } = await fetchItem({ variables: { id: itemId } })

return data?.item
} catch (e) {
console.error(e)
return null
}
}

const relayError = (failedRelays) => {
return new Promise(resolve => {
const handleSkip = () => {
Expand Down Expand Up @@ -139,12 +193,13 @@ export default function useCrossposter () {
return toaster.danger(`Error crossposting: ${errorMessage}`)
}

async function handleEventCreation (item) {
async function handleEventCreation (item, fetchItemData) {
const determineItemType = (item) => {
const typeMap = {
url: 'link',
bounty: 'bounty',
pollCost: 'poll'
pollCost: 'poll',
parentId: 'reply'
}

for (const [key, type] of Object.entries(typeMap)) {
Expand All @@ -168,28 +223,19 @@ export default function useCrossposter () {
return await bountyToEvent(item)
case 'poll':
return await pollToEvent(item)
case 'reply':
return await replyToEvent(item, fetchItemData)
default:
return crosspostError('Unknown item type')
}
}

const fetchItemData = async (itemId) => {
try {
const { data } = await fetchItem({ variables: { id: itemId } })

return data?.item
} catch (e) {
console.error(e)
return null
}
}

const crosspostItem = async item => {
const crosspostItem = async (item) => {
let failedRelays
let allSuccessful = false
let noteId

const event = await handleEventCreation(item)
const event = await handleEventCreation(item, fetchItemData)
if (!event) return { allSuccessful, noteId }

do {
Expand Down
1 change: 1 addition & 0 deletions fragments/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const COMMENT_FIELDS = gql`
meDontLikeSats
meBookmark
meSubscription
noteId
outlawed
freebie
path
Expand Down
Loading