Nostr crossposting improvements (#629)
* Add nostr event id field to items * crosspost-item * crosspost old items, update with nEventId * Updating noteId encoding, cleaning up a little * Fixing item-info condition, cleaning up * Linting * Spacing nit * Add createdAt variable back * Change instances of eventId to noteId * Adding upsertNoteId mutation * Cleaning up updateItem, using toasts to communivate success/failure in crosspost-item * Linting * Fix type * Move crosspost to share button, make sure only OP can crosspost * Lint * Simplify conditions * user might have no nostr extension installed Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com> * change upsertNoteId to updateNoteID for resolver and mutations, change isOp to mine, remove unused noteId params * Use nostr.com for linking out with noteId * lint * add noopener to window.open call * Simplify condition, throw GraphQLError --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com>
This commit is contained in:
parent
bfcca7d34e
commit
5737027c0f
|
@ -724,6 +724,18 @@ export default {
|
|||
return item
|
||||
}
|
||||
},
|
||||
updateNoteId: async (parent, { id, noteId }, { models }) => {
|
||||
if (!id) {
|
||||
throw new GraphQLError('id required', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
await models.item.update({
|
||||
where: { id: Number(id) },
|
||||
data: { noteId }
|
||||
})
|
||||
|
||||
return { id, noteId }
|
||||
},
|
||||
pollVote: async (parent, { id, hash, hmac }, { me, models, lnd }) => {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
|
@ -1200,7 +1212,7 @@ const getForwardUsers = async (models, forward) => {
|
|||
export const SELECT =
|
||||
`SELECT "Item".id, "Item".created_at, "Item".created_at as "createdAt", "Item".updated_at,
|
||||
"Item".updated_at as "updatedAt", "Item".title, "Item".text, "Item".url, "Item"."bounty",
|
||||
"Item"."userId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
|
||||
"Item"."noteId", "Item"."userId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
|
||||
"Item"."rootId", "Item".upvotes, "Item".company, "Item".location, "Item".remote, "Item"."deletedAt",
|
||||
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats,
|
||||
"Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes",
|
||||
|
|
|
@ -32,6 +32,7 @@ export default gql`
|
|||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int, hash: String, hmac: String): Item!
|
||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
|
||||
updateNoteId(id: ID!, noteId: String!): Item!
|
||||
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
|
||||
dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Boolean!
|
||||
act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
|
||||
|
@ -83,6 +84,7 @@ export default gql`
|
|||
boost: Int!
|
||||
bounty: Int
|
||||
bountyPaidTo: [Int]
|
||||
noteId: String
|
||||
sats: Int!
|
||||
commentSats: Int!
|
||||
lastCommentAt: Date
|
||||
|
|
|
@ -40,6 +40,16 @@ export function DiscussionForm ({
|
|||
}`
|
||||
)
|
||||
|
||||
const [updateNoteId] = useMutation(
|
||||
gql`
|
||||
mutation updateNoteId($id: ID!, $noteId: String!) {
|
||||
updateNoteId(id: $id, noteId: $noteId) {
|
||||
id
|
||||
noteId
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async ({ boost, crosspost, ...values }) => {
|
||||
try {
|
||||
|
@ -64,14 +74,27 @@ export function DiscussionForm ({
|
|||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
|
||||
let noteId = null
|
||||
const discussionId = data?.upsertDiscussion?.id
|
||||
|
||||
try {
|
||||
if (crosspost && data?.upsertDiscussion?.id) {
|
||||
await crossposter({ ...values, id: data.upsertDiscussion.id })
|
||||
if (crosspost && discussionId) {
|
||||
const crosspostResult = await crossposter({ ...values, id: discussionId })
|
||||
noteId = crosspostResult?.noteId
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (noteId) {
|
||||
await updateNoteId({
|
||||
variables: {
|
||||
id: discussionId,
|
||||
noteId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (item) {
|
||||
await router.push(`/items/${item.id}`)
|
||||
} else {
|
||||
|
|
|
@ -145,6 +145,11 @@ export default function ItemInfo ({
|
|||
</Link>}
|
||||
{me && !item.meSats && !item.position &&
|
||||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
||||
{me && item?.noteId && (
|
||||
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
|
||||
nostr note
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{item.mine && !item.position && !item.deletedAt && !item.bio &&
|
||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
||||
{me && !item.mine &&
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import ShareIcon from '../svgs/share-fill.svg'
|
||||
import copy from 'clipboard-copy'
|
||||
import useCrossposter from './use-crossposter'
|
||||
import { useMutation, gql } from '@apollo/client'
|
||||
import { useMe } from './me'
|
||||
import { useToast } from './toast'
|
||||
import { SSR } from '../lib/constants'
|
||||
|
@ -15,9 +17,22 @@ const getShareUrl = (item, me) => {
|
|||
|
||||
export default function Share ({ item }) {
|
||||
const me = useMe()
|
||||
const crossposter = useCrossposter()
|
||||
const toaster = useToast()
|
||||
const url = getShareUrl(item, me)
|
||||
|
||||
const mine = item?.user?.id === me?.id
|
||||
|
||||
const [updateNoteId] = useMutation(
|
||||
gql`
|
||||
mutation updateNoteId($id: ID!, $noteId: String!) {
|
||||
updateNoteId(id: $id, noteId: $noteId) {
|
||||
id
|
||||
noteId
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
return !SSR && navigator?.share
|
||||
? (
|
||||
<div className='ms-auto pointer d-flex align-items-center'>
|
||||
|
@ -58,6 +73,42 @@ export default function Share ({ item }) {
|
|||
>
|
||||
copy link
|
||||
</Dropdown.Item>
|
||||
{mine && !item?.noteId && (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (!(await window.nostr?.getPublicKey())) {
|
||||
throw new Error('not available')
|
||||
}
|
||||
} catch (e) {
|
||||
toaster.danger(`Nostr extension error: ${e.message}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (item?.id) {
|
||||
const crosspostResult = await crossposter({ ...item })
|
||||
const noteId = crosspostResult?.noteId
|
||||
if (noteId) {
|
||||
await updateNoteId({
|
||||
variables: {
|
||||
id: item.id,
|
||||
noteId
|
||||
}
|
||||
})
|
||||
}
|
||||
toaster.success('Crosspost successful')
|
||||
} else {
|
||||
toaster.warning('Item ID not available')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toaster.danger('Crosspost failed')
|
||||
}
|
||||
}}
|
||||
>
|
||||
crosspost to nostr
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ async function discussionToEvent (item) {
|
|||
export default function useCrossposter () {
|
||||
const toast = useToast()
|
||||
const { data } = useQuery(SETTINGS)
|
||||
const relays = [...DEFAULT_CROSSPOSTING_RELAYS, ...(data?.settings?.nostRelays || [])]
|
||||
const relays = [...DEFAULT_CROSSPOSTING_RELAYS, ...(data?.settings?.nostrRelays || [])]
|
||||
|
||||
const relayError = (failedRelays) => {
|
||||
return new Promise(resolve => {
|
||||
|
@ -53,12 +53,15 @@ export default function useCrossposter () {
|
|||
return useCallback(async item => {
|
||||
let failedRelays
|
||||
let allSuccessful = false
|
||||
let noteId
|
||||
|
||||
do {
|
||||
// XXX we only use discussions right now
|
||||
const event = await discussionToEvent(item)
|
||||
const result = await crosspost(event, failedRelays || relays)
|
||||
|
||||
noteId = result.noteId
|
||||
|
||||
failedRelays = result.failedRelays.map(relayObj => relayObj.relay)
|
||||
|
||||
if (failedRelays.length > 0) {
|
||||
|
@ -73,6 +76,6 @@ export default function useCrossposter () {
|
|||
}
|
||||
} while (failedRelays.length > 0)
|
||||
|
||||
return { allSuccessful }
|
||||
return { allSuccessful, noteId }
|
||||
}, [relays, toast])
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export const ITEM_FIELDS = gql`
|
|||
boost
|
||||
bounty
|
||||
bountyPaidTo
|
||||
noteId
|
||||
path
|
||||
upvotes
|
||||
meSats
|
||||
|
|
|
@ -98,9 +98,9 @@ export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
|||
}
|
||||
})
|
||||
|
||||
const eventId = hexToBech32(signedEvent.id, 'nevent')
|
||||
const noteId = hexToBech32(signedEvent.id, 'note')
|
||||
|
||||
return { successfulRelays, failedRelays, eventId }
|
||||
return { successfulRelays, failedRelays, noteId }
|
||||
} catch (error) {
|
||||
console.error('Crosspost discussion error:', error)
|
||||
return { error }
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE "Item" ADD COLUMN "noteId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Item.noteId_unique" ON "Item"("noteId");
|
|
@ -298,6 +298,7 @@ model Item {
|
|||
otsHash String?
|
||||
imgproxyUrls Json?
|
||||
bounty Int?
|
||||
noteId String? @unique(map: "Item.noteId_unique")
|
||||
rootId Int?
|
||||
bountyPaidTo Int[]
|
||||
upvotes Int @default(0)
|
||||
|
|
Loading…
Reference in New Issue