This commit is contained in:
keyan 2023-01-12 17:53:09 -06:00
parent ed153b5199
commit 10ff3fa1c3
18 changed files with 249 additions and 71 deletions

View File

@ -159,7 +159,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1
AND "pinId" IS NULL
AND "pinId" IS NULL AND "deletedAt" IS NULL
${topClause(when)}
${await filterClause(me, models)}
${await topOrderClause(sort, me, models)}
@ -176,7 +176,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NOT NULL
AND "Item".created_at <= $1
AND "Item".created_at <= $1 AND "deletedAt" IS NULL
${topClause(when)}
${await filterClause(me, models)}
${await topOrderClause(sort, me, models)}
@ -239,7 +239,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1
AND "pinId" IS NULL
AND "pinId" IS NULL AND "deletedAt" IS NULL
${topClause(within)}
${await filterClause(me, models)}
${await topOrderByWeightedSats(me, models)}
@ -288,7 +288,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1 AND "Item".created_at > $3
AND "pinId" IS NULL AND NOT bio
AND "pinId" IS NULL AND NOT bio AND "deletedAt" IS NULL
${subClause(4)}
${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)}
@ -301,7 +301,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1
AND "pinId" IS NULL AND NOT bio
AND "pinId" IS NULL AND NOT bio AND "deletedAt" IS NULL
${subClause(3)}
${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)}
@ -438,7 +438,7 @@ export default {
comments = await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE "parentId" IS NOT NULL
WHERE "parentId" IS NOT NULL AND "deletedAt" IS NULL
AND "Item".created_at <= $1
${topClause(within)}
${await filterClause(me, models)}
@ -548,6 +548,28 @@ export default {
},
Mutation: {
deleteItem: async (parent, { id }, { me, models }) => {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new AuthenticationError('item does not belong to you')
}
const data = { deletedAt: new Date() }
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, args, { me, models }) => {
const { id, ...data } = args
data.url = ensureProtocol(data.url)
@ -1040,7 +1062,7 @@ function nestComments (flat, parentId) {
export const SELECT =
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
"Item".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
"Item".company, "Item".location, "Item".remote,
"Item".company, "Item".location, "Item".remote, "Item"."deletedAt",
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost",
"Item".msats, "Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes",
"Item"."weightedDownVotes", "Item".freebie, ltree2text("Item"."path") AS "path"`

View File

@ -31,6 +31,7 @@ export default gql`
}
extend type Mutation {
deleteItem(id: ID): Item
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
upsertDiscussion(id: ID, title: String!, text: String, boost: Int, forward: String): Item!
upsertJob(id: ID, sub: ID!, title: String!, company: String!, location: String, remote: Boolean,
@ -71,6 +72,7 @@ export default gql`
id: ID!
createdAt: String!
updatedAt: String!
deletedAt: String
title: String
searchTitle: String
url: String

View File

@ -4,6 +4,8 @@ import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css'
import TextareaAutosize from 'react-textarea-autosize'
import { EditFeeButton } from './fee-button'
import { Button } from 'react-bootstrap'
import Delete from './delete'
export const CommentSchema = Yup.object({
text: Yup.string().required('required').trim()
@ -54,10 +56,15 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
autoFocus
required
/>
<EditFeeButton
paidSats={comment.meSats}
parentId={comment.parentId} text='save' ChildButton={SubmitButton} variant='secondary'
/>
<div className='d-flex justify-content-between'>
<Delete itemId={comment.id} onDelete={onSuccess}>
<Button variant='grey-medium'>delete</Button>
</Delete>
<EditFeeButton
paidSats={comment.meSats}
parentId={comment.parentId} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</div>
</Form>
</div>
)

View File

@ -19,6 +19,7 @@ import Flag from '../svgs/flag-fill.svg'
import { Badge } from 'react-bootstrap'
import { abbrNum } from '../lib/format'
import Share from './share'
import { DeleteDropdown } from './delete'
function Parent ({ item, rootText }) {
const ParentFrag = () => (
@ -138,7 +139,7 @@ export default function Comment ({
{me && !item.meSats && !item.meDontLike && !item.mine && <DontLikeThis id={item.id} />}
{(item.outlawed && <Link href='/outlawed'><a>{' '}<Badge className={itemStyles.newComment} variant={null}>OUTLAWED</Badge></a></Link>) ||
(item.freebie && !item.mine && (me?.greeterMode) && <Link href='/freebie'><a>{' '}<Badge className={itemStyles.newComment} variant={null}>FREEBIE</Badge></a></Link>)}
{canEdit &&
{canEdit && !item.deletedAt &&
<>
<span> \ </span>
<div
@ -156,6 +157,7 @@ export default function Comment ({
/>
</div>
</>}
{mine && !canEdit && !item.deletedAt && <DeleteDropdown itemId={item.id} />}
</div>
{!includeParent && (collapse
? <Eye

103
components/delete.js Normal file
View File

@ -0,0 +1,103 @@
import { useMutation } from '@apollo/client'
import { gql } from 'apollo-server-micro'
import { useState } from 'react'
import { Alert, Button, Dropdown } from 'react-bootstrap'
import { useShowModal } from './modal'
import MoreIcon from '../svgs/more-fill.svg'
export default function Delete ({ itemId, children, onDelete }) {
const showModal = useShowModal()
const [deleteItem] = useMutation(
gql`
mutation deleteItem($id: ID!) {
deleteItem(id: $id) {
text
title
url
pollCost
deletedAt
}
}`, {
update (cache, { data: { deleteItem } }) {
console.log(deleteItem)
cache.modify({
id: `Item:${itemId}`,
fields: {
text: () => deleteItem.text,
title: () => deleteItem.title,
url: () => deleteItem.url,
pollCost: () => deleteItem.pollCost,
deletedAt: () => deleteItem.deletedAt
}
})
}
}
)
return (
<span
className='pointer' onClick={() => {
showModal(onClose => {
return (
<DeleteConfirm
onConfirm={async () => {
const { error } = await deleteItem({ variables: { id: itemId } })
if (error) {
throw new Error({ message: error.toString() })
}
if (onDelete) {
onDelete()
}
onClose()
}}
/>
)
})
}}
>{children}
</span>
)
}
function DeleteConfirm ({ onConfirm }) {
const [error, setError] = useState()
return (
<>
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
<p className='font-weight-bolder'>Are you sure? This is a gone forever kind of delete.</p>
<div className='d-flex justify-content-end'>
<Button
variant='danger' onClick={async () => {
try {
await onConfirm()
} catch (e) {
setError(e.message || e)
}
}}
>delete
</Button>
</div>
</>
)
}
export function DeleteDropdown (props) {
return (
<Dropdown className='pointer' as='span'>
<Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
<MoreIcon className='fill-grey ml-1' height={16} width={16} />
</Dropdown.Toggle>
<Dropdown.Menu>
<Delete {...props}>
<Dropdown.Item
className='text-center'
>
delete
</Dropdown.Item>
</Delete>
</Dropdown.Menu>
</Dropdown>
)
}

View File

@ -10,6 +10,8 @@ import FeeButton, { EditFeeButton } from './fee-button'
import { ITEM_FIELDS } from '../fragments/items'
import AccordianItem from './accordian-item'
import Item from './item'
import Delete from './delete'
import { Button } from 'react-bootstrap'
export function DiscussionForm ({
item, editThreshold, titleLabel = 'title',
@ -103,27 +105,34 @@ export function DiscussionForm ({
{adv && <AdvPostForm edit={!!item} />}
<div className='mt-3'>
{item
? <EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
? (
<div className='d-flex justify-content-between'>
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
<Button variant='grey-medium'>delete</Button>
</Delete>
<EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</div>)
: <FeeButton
baseFee={1} parentId={null} text={buttonText}
ChildButton={SubmitButton} variant='secondary'
/>}
</div>
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>similar</div>}
body={
<div>
{related.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
{!item &&
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>similar</div>}
body={
<div>
{related.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
}
/>
</div>
/>
</div>}
</Form>
)
}

View File

@ -16,6 +16,7 @@ import DontLikeThis from './dont-link-this'
import Flag from '../svgs/flag-fill.svg'
import Share from './share'
import { abbrNum } from '../lib/format'
import { DeleteDropdown } from './delete'
export function SearchTitle ({ title }) {
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
@ -123,7 +124,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
</Link>
</>}
</span>
{canEdit &&
{canEdit && !item.deletedAt &&
<>
<span> \ </span>
<Link href={`/items/${item.id}/edit`} passHref>
@ -139,6 +140,8 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
</a>
</Link>
</>}
{mine && !canEdit && !item.position && !item.deletedAt &&
<DeleteDropdown itemId={item.id} />}
</div>
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
</div>

View File

@ -10,6 +10,8 @@ import AccordianItem from './accordian-item'
import { MAX_TITLE_LENGTH } from '../lib/constants'
import { URL_REGEXP } from '../lib/url'
import FeeButton, { EditFeeButton } from './fee-button'
import Delete from './delete'
import { Button } from 'react-bootstrap'
export function LinkForm ({ item, editThreshold }) {
const router = useRouter()
@ -138,42 +140,51 @@ export function LinkForm ({ item, editThreshold }) {
<AdvPostForm edit={!!item} />
<div className='mt-3'>
{item
? <EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
? (
<div className='d-flex justify-content-between'>
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
<Button variant='grey-medium'>delete</Button>
</Delete>
<EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</div>)
: <FeeButton
baseFee={1} parentId={null} text='post'
ChildButton={SubmitButton} variant='secondary'
/>}
</div>
{dupesData?.dupes?.length > 0 &&
<div className='mt-3'>
<AccordianItem
show
headerColor='#c03221'
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>dupes</div>}
body={
<div>
{dupesData.dupes.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
{!item &&
<>
{dupesData?.dupes?.length > 0 &&
<div className='mt-3'>
<AccordianItem
show
headerColor='#c03221'
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>dupes</div>}
body={
<div>
{dupesData.dupes.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
}
/>
</div>}
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>similar</div>}
body={
<div>
{related.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
/>
</div>}
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>similar</div>}
body={
<div>
{related.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
}
/>
</div>
/>
</div>
</>}
</Form>
)
}

View File

@ -83,7 +83,7 @@ function Notification ({ n }) {
{n.sources.tips > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{n.sources.tips} sats for tipping top content early</span>}
</div>}
<div className='pb-1' style={{ lineHeight: '140%' }}>
SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards' passHref><a>here</a></Link>.
SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards' passHref><a>here</a></Link>.
</div>
</div>
</div>

View File

@ -7,6 +7,8 @@ import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
import { MAX_TITLE_LENGTH, MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES } from '../lib/constants'
import TextareaAutosize from 'react-textarea-autosize'
import FeeButton, { EditFeeButton } from './fee-button'
import Delete from './delete'
import { Button } from 'react-bootstrap'
export function PollForm ({ item, editThreshold }) {
const router = useRouter()
@ -94,10 +96,16 @@ export function PollForm ({ item, editThreshold }) {
<AdvPostForm edit={!!item} />
<div className='mt-3'>
{item
? <EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
? (
<div className='d-flex justify-content-between'>
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
<Button variant='grey-medium'>delete</Button>
</Delete>
<EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</div>)
: <FeeButton
baseFee={1} parentId={null} text='post'
ChildButton={SubmitButton} variant='secondary'

View File

@ -171,6 +171,8 @@ export default function UpVote ({ item, className }) {
return `${sats} sat${sats > 1 ? 's' : ''}`
}
const disabled = item?.mine || fwd2me || item?.deletedAt
const color = getColor(item?.meSats)
return (
<LightningConsumer>
@ -182,7 +184,7 @@ export default function UpVote ({ item, className }) {
if (!item) return
// we can't tip ourselves
if (item?.mine || fwd2me) {
if (disabled) {
return
}
@ -197,7 +199,7 @@ export default function UpVote ({ item, className }) {
if (!item) return
// we can't tip ourselves
if (item?.mine || fwd2me) {
if (disabled) {
return
}
@ -234,9 +236,9 @@ export default function UpVote ({ item, className }) {
})
}
>
<ActionTooltip notForm disable={item?.mine || fwd2me} overlayText={overlayText()}>
<ActionTooltip notForm disable={disabled} overlayText={overlayText()}>
<div
className={`${item?.mine || fwd2me ? styles.noSelfTips : ''}
className={`${disabled ? styles.noSelfTips : ''}
${styles.upvoteWrapper}`}
>
<UpBolt
@ -245,7 +247,7 @@ export default function UpVote ({ item, className }) {
className={
`${styles.upvote}
${className || ''}
${item?.mine || fwd2me ? styles.noSelfTips : ''}
${disabled ? styles.noSelfTips : ''}
${item?.meSats ? styles.voted : ''}`
}
style={item?.meSats

View File

@ -5,6 +5,7 @@ export const COMMENT_FIELDS = gql`
id
parentId
createdAt
deletedAt
text
user {
name

View File

@ -6,6 +6,7 @@ export const ITEM_FIELDS = gql`
id
parentId
createdAt
deletedAt
title
url
user {

View File

@ -1,9 +1,9 @@
export function ignoreClick (e) {
return e.target.onclick || // the target has a click handler
// the target has an interactive parent
e.target.matches(':where(.upvoteParent, form, textarea, button, a, input) :scope') ||
e.target.matches(':where(.upvoteParent, .pointer, form, textarea, button, a, input) :scope') ||
// the target is an interactive element
['TEXTAREA', 'BUTTON', 'A', 'INPUT', 'FORM'].includes(e.target.tagName.toUpperCase()) ||
// the target is an interactive element
e.target.class === 'upvoteParent'
e.target.class === 'upvoteParent' || e.target.class === 'pointer'
}

View File

@ -15,6 +15,7 @@ import { useRouter } from 'next/router'
import Item from '../components/item'
import Comment from '../components/comment'
import React from 'react'
import ItemJob from '../components/item-job'
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
@ -92,7 +93,7 @@ function Detail ({ fact }) {
return (
<>
<div className={satusClass(fact.status)}>
SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards' passHref><a>here</a></Link>.
SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards' passHref><a>here</a></Link>.
</div>
</>
)
@ -128,6 +129,9 @@ function Detail ({ fact }) {
}
if (fact.item.title) {
if (fact.item.isJob) {
return <ItemJob className={styles.itemWrapper} item={fact.item} />
}
return <div className={styles.itemWrapper}><Item item={fact.item} /></div>
}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Item" ADD COLUMN "deletedAt" TIMESTAMP(3);

View File

@ -210,6 +210,7 @@ model Item {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
deletedAt DateTime?
title String?
text String?
url String?

View File

@ -53,7 +53,7 @@ function earn ({ models }) {
FROM
"Item"
WHERE created_at >= now_utc() - interval '36 hours'
AND "weightedVotes" > 0
AND "weightedVotes" > 0 AND "deletedAt" IS NULL AND NOT bio
) x
WHERE x.percentile <= ${TOP_PERCENTILE}
),