Ephemeral item support (#570)
backend impl and some of the UI for ephemeral item support more to come, this is just a WIP so far Consolidate client-side ephemeral fee logic in FeeButton components for easier reuse * update the update_item function to handle the case where an item was not ephemeral, but now is, so we charge the user accordingly * introduce `hasDeleteCommand` for a better logical abstraction for some use cases in the code * introduce `EPHEMERAL_FEE_SATS` which is derived from `EPHEMERAL_FEE_MSATS`, so we don't have to the same calculation over and over Remove fees for ephemeral items * remove unused markdownField prop in FeeButton * remove empty migration minor code cleanup Centralize delete item by author code to reduce duplication
This commit is contained in:
		
							parent
							
								
									e713387920
								
							
						
					
					
						commit
						1d394bebe1
					
				@ -16,7 +16,7 @@ import { parse } from 'tldts'
 | 
				
			|||||||
import uu from 'url-unshort'
 | 
					import uu from 'url-unshort'
 | 
				
			||||||
import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
 | 
					import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
 | 
				
			||||||
import { sendUserNotification } from '../webPush'
 | 
					import { sendUserNotification } from '../webPush'
 | 
				
			||||||
import { defaultCommentSort, isJob } from '../../lib/item'
 | 
					import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
 | 
				
			||||||
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
 | 
					import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
 | 
				
			||||||
import { datePivot } from '../../lib/time'
 | 
					import { datePivot } from '../../lib/time'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -654,21 +654,7 @@ export default {
 | 
				
			|||||||
        throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
 | 
					        throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const data = { deletedAt: new Date() }
 | 
					      return await deleteItemByAuthor({ models, id, item: old })
 | 
				
			||||||
      if (old.text) {
 | 
					 | 
				
			||||||
        data.text = '*deleted by author*'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (old.title) {
 | 
					 | 
				
			||||||
        data.title = 'deleted by author'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (old.url) {
 | 
					 | 
				
			||||||
        data.url = null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (old.pollCost) {
 | 
					 | 
				
			||||||
        data.pollCost = null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return await models.item.update({ where: { id: Number(id) }, data })
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    upsertLink: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
 | 
					    upsertLink: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
 | 
				
			||||||
      await ssValidate(linkSchema, item, { models, me })
 | 
					      await ssValidate(linkSchema, item, { models, me })
 | 
				
			||||||
@ -1109,6 +1095,12 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await createMentions(item, models)
 | 
					  await createMentions(item, models)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hasDeleteCommand(old.text)) {
 | 
				
			||||||
 | 
					    // delete any deletion jobs that were created from a prior version of the item
 | 
				
			||||||
 | 
					    await clearDeletionJobs(item, models)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await enqueueDeletionJob(item, models)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  item.comments = []
 | 
					  item.comments = []
 | 
				
			||||||
  return item
 | 
					  return item
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1138,12 +1130,27 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await createMentions(item, models)
 | 
					  await createMentions(item, models)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await enqueueDeletionJob(item, models)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notifyUserSubscribers({ models, item })
 | 
					  notifyUserSubscribers({ models, item })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  item.comments = []
 | 
					  item.comments = []
 | 
				
			||||||
  return item
 | 
					  return item
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const clearDeletionJobs = async (item, models) => {
 | 
				
			||||||
 | 
					  await models.$queryRawUnsafe(`DELETE FROM pgboss.job WHERE name = 'deleteItem' AND data->>'id' = '${item.id}';`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const enqueueDeletionJob = async (item, models) => {
 | 
				
			||||||
 | 
					  const deleteCommand = getDeleteCommand(item.text)
 | 
				
			||||||
 | 
					  if (deleteCommand) {
 | 
				
			||||||
 | 
					    await models.$queryRawUnsafe(`
 | 
				
			||||||
 | 
					      INSERT INTO pgboss.job (name, data, startafter)
 | 
				
			||||||
 | 
					      VALUES ('deleteItem', jsonb_build_object('id', ${item.id}), now() + interval '${deleteCommand.number} ${deleteCommand.unit}s');`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getForwardUsers = async (models, forward) => {
 | 
					const getForwardUsers = async (models, forward) => {
 | 
				
			||||||
  const fwdUsers = []
 | 
					  const fwdUsers = []
 | 
				
			||||||
  if (forward) {
 | 
					  if (forward) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										35
									
								
								lib/item.js
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								lib/item.js
									
									
									
									
									
								
							@ -11,3 +11,38 @@ export const defaultCommentSort = (pinned, bio, createdAt) => {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isJob = item => typeof item.maxBid !== 'undefined'
 | 
					export const isJob = item => typeof item.maxBid !== 'undefined'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getDeleteCommand = (text = '') => {
 | 
				
			||||||
 | 
					  const matches = [...text.matchAll(deletePattern)]
 | 
				
			||||||
 | 
					  const commands = matches?.map(match => ({ number: match[1], unit: match[2] }))
 | 
				
			||||||
 | 
					  return commands.length ? commands[commands.length - 1] : undefined
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const hasDeleteCommand = (text) => !!getDeleteCommand(text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteItemByAuthor = async ({ models, id, item }) => {
 | 
				
			||||||
 | 
					  if (!item) {
 | 
				
			||||||
 | 
					    item = await models.item.findUnique({ where: { id: Number(id) } })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!item) {
 | 
				
			||||||
 | 
					    console.log('attempted to delete an item that does not exist', id)
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const updateData = { deletedAt: new Date() }
 | 
				
			||||||
 | 
					  if (item.text) {
 | 
				
			||||||
 | 
					    updateData.text = '*deleted by author*'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.title) {
 | 
				
			||||||
 | 
					    updateData.title = 'deleted by author'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.url) {
 | 
				
			||||||
 | 
					    updateData.url = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.pollCost) {
 | 
				
			||||||
 | 
					    updateData.pollCost = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return await models.item.update({ where: { id: Number(id) }, data: updateData })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								worker/ephemeralItems.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								worker/ephemeralItems.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { deleteItemByAuthor } from '../lib/item.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function deleteItem ({ models }) {
 | 
				
			||||||
 | 
					  return async function ({ data: eventData }) {
 | 
				
			||||||
 | 
					    console.log('deleteItem', eventData)
 | 
				
			||||||
 | 
					    const { id } = eventData
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await deleteItemByAuthor({ models, id })
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      console.error('failed to delete item', err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -15,6 +15,7 @@ import fetch from 'cross-fetch'
 | 
				
			|||||||
import { authenticatedLndGrpc } from 'ln-service'
 | 
					import { authenticatedLndGrpc } from 'ln-service'
 | 
				
			||||||
import { views, rankViews } from './views.js'
 | 
					import { views, rankViews } from './views.js'
 | 
				
			||||||
import { imgproxy } from './imgproxy.js'
 | 
					import { imgproxy } from './imgproxy.js'
 | 
				
			||||||
 | 
					import { deleteItem } from './ephemeralItems.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { loadEnvConfig } = nextEnv
 | 
					const { loadEnvConfig } = nextEnv
 | 
				
			||||||
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
 | 
					const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
 | 
				
			||||||
@ -68,6 +69,7 @@ async function work () {
 | 
				
			|||||||
  await boss.work('views', views(args))
 | 
					  await boss.work('views', views(args))
 | 
				
			||||||
  await boss.work('rankViews', rankViews(args))
 | 
					  await boss.work('rankViews', rankViews(args))
 | 
				
			||||||
  await boss.work('imgproxy', imgproxy(args))
 | 
					  await boss.work('imgproxy', imgproxy(args))
 | 
				
			||||||
 | 
					  await boss.work('deleteItem', deleteItem(args))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log('working jobs')
 | 
					  console.log('working jobs')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user