Skip to content

Commit 5544450

Browse files
committed
OldItem trigger, item history; Shadow edits; items can always be edited; don't record bios
1 parent c571ba0 commit 5544450

File tree

16 files changed

+251
-42
lines changed

16 files changed

+251
-42
lines changed

api/paidAction/itemUpdate.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export async function perform (args, context) {
4040
}
4141
})
4242

43+
console.log(data)
44+
console.log(old)
45+
4346
const newBoost = boost - old.boost
4447
const itemActs = []
4548
if (newBoost > 0) {
@@ -59,7 +62,7 @@ export async function perform (args, context) {
5962
const mentions = await getMentions(args, context)
6063
const itemMentions = await getItemMentions(args, context)
6164
const itemUploads = uploadIds.map(id => ({ uploadId: id }))
62-
65+
console.log(old)
6366
await tx.upload.updateMany({
6467
where: { id: { in: uploadIds } },
6568
data: { paid: true }

api/resolvers/item.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,12 @@ export default {
12061206
}
12071207
return await models.user.findUnique({ where: { id: item.userId } })
12081208
},
1209+
oldVersions: async (item, args, { models }) => {
1210+
return await models.oldItem.findMany({
1211+
where: { originalItemId: item.id },
1212+
orderBy: { cloneDiedAt: 'desc' }
1213+
})
1214+
},
12091215
forwards: async (item, args, { models }) => {
12101216
return await models.itemForward.findMany({
12111217
where: {
@@ -1487,13 +1493,12 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
14871493

14881494
const user = await models.user.findUnique({ where: { id: meId } })
14891495

1490-
// edits are only allowed for own items within 10 minutes
1491-
// but forever if an admin is editing an "admin item", it's their bio or a job
1496+
// edit always allowed for own items
1497+
// or if it's an admin item, their bio or a job TODO: adjust every edit
14921498
const myBio = user.bioId === old.id
1493-
const timer = Date.now() < datePivot(new Date(old.invoicePaidAt ?? old.createdAt), { seconds: ITEM_EDIT_SECONDS })
1494-
const canEdit = (timer && ownerEdit) || adminEdit || myBio || isJob(old)
1499+
const canEdit = ownerEdit || adminEdit || myBio || isJob(old)
14951500
if (!canEdit) {
1496-
throw new GqlInputError('item can no longer be edited')
1501+
throw new GqlInputError('item cannot be edited')
14971502
}
14981503

14991504
if (item.url && !isJob(item)) {
@@ -1515,7 +1520,6 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
15151520

15161521
// never change author of item
15171522
item.userId = old.userId
1518-
15191523
const resultItem = await performPaidAction('ITEM_UPDATE', item, { models, me, lnd })
15201524

15211525
resultItem.comments = []

api/typeDefs/item.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ export default gql`
9090
ad: Item
9191
}
9292
93+
type OldItem {
94+
id: ID!
95+
title: String
96+
text: String
97+
url: String
98+
createdAt: Date
99+
updatedAt: Date
100+
cloneBornAt: Date
101+
cloneDiedAt: Date
102+
}
103+
93104
type Comments {
94105
cursor: String
95106
comments: [Item!]!
@@ -165,6 +176,9 @@ export default gql`
165176
parentOtsHash: String
166177
forwards: [ItemForward]
167178
imgproxyUrls: JSONObject
179+
cloneBornAt: Date
180+
cloneDiedAt: Date
181+
oldVersions: [OldItem!]
168182
rel: String
169183
apiKey: Boolean
170184
invoice: Invoice

components/comment.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ export default function Comment ({
201201
</>
202202
}
203203
edit={edit}
204-
toggleEdit={e => { setEdit(!edit) }}
205-
editText={edit ? 'cancel' : 'edit'}
204+
toggleShadowEdit={e => { setEdit(!edit) }}
205+
shadowEditText={edit ? 'cancel' : 'edit'}
206206
/>}
207207

208208
{!includeParent && (collapse === 'yep'

components/discussion-form.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { UPSERT_DISCUSSION } from '@/fragments/paidAction'
1616
import useItemSubmit from './use-item-submit'
1717

1818
export function DiscussionForm ({
19-
item, sub, editThreshold, titleLabel = 'title',
19+
item, sub, shadowEditThreshold, titleLabel = 'title',
2020
textLabel = 'text',
2121
handleSubmit, children
2222
}) {
@@ -75,8 +75,8 @@ export function DiscussionForm ({
7575
label={<>{textLabel} <small className='text-muted ms-2'>optional</small></>}
7676
name='text'
7777
minRows={6}
78-
hint={editThreshold
79-
? <div className='text-muted fw-bold font-monospace'><Countdown date={editThreshold} /></div>
78+
hint={shadowEditThreshold // TODO: when countdown expires don't show it, we need the countdown to know if can shadow edit
79+
? <div className='text-muted fw-bold font-monospace'><Countdown date={shadowEditThreshold} /></div>
8080
: null}
8181
/>
8282
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} sub={sub} />

components/item-history.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { timeSince } from '@/lib/time'
2+
import styles from './item.module.css'
3+
4+
// TODO: PAID add a button to restore the item to the version
5+
// TODO: styling
6+
// TODO: render it as Item
7+
8+
export default function ItemHistory ({ item }) {
9+
return (
10+
<div className={styles.other}>
11+
{item.oldVersions.map(version => (
12+
<div key={version.id}>
13+
<span className='text-muted' title={version.cloneBornAt || version.createdAt}>{timeSince(new Date(version.cloneBornAt || version.createdAt))}</span>
14+
<h3>{version.title}</h3>
15+
<p>{version.text}</p>
16+
<p>{version.url}</p>
17+
</div>
18+
))}
19+
</div>
20+
)
21+
}

components/item-info.js

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { useToast } from './toast'
2828
import { useShowModal } from './modal'
2929
import classNames from 'classnames'
3030
import SubPopover from './sub-popover'
31-
import useCanEdit from './use-can-edit'
31+
import useCanShadowEdit from './use-can-edit'
32+
import ItemHistory from './item-history'
3233

3334
function itemTitle (item) {
3435
let title = ''
@@ -65,16 +66,17 @@ function itemTitle (item) {
6566

6667
export default function ItemInfo ({
6768
item, full, commentsText = 'comments',
68-
commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleEdit, editText,
69+
commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleShadowEdit, shadowEditText,
6970
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
7071
setDisableRetry, disableRetry
7172
}) {
7273
const { me } = useMe()
74+
const showModal = useShowModal()
7375
const router = useRouter()
7476
const [hasNewComments, setHasNewComments] = useState(false)
7577
const root = useRoot()
7678
const sub = item?.sub || root?.sub
77-
const [canEdit, setCanEdit, editThreshold] = useCanEdit(item)
79+
const [canShadowEdit, setCanShadowEdit, shadowEditThreshold] = useCanShadowEdit(item)
7880

7981
useEffect(() => {
8082
if (!full) {
@@ -145,6 +147,13 @@ export default function ItemInfo ({
145147
yesterday
146148
</Link>
147149
</>}
150+
{item.oldVersions?.length > 0 &&
151+
<>
152+
<span> \ </span>
153+
<span onClick={() => showModal((onClose) => <ItemHistory item={item} onClose={onClose} />)} className='text-reset' title={item.cloneBornAt}>
154+
edited {item.oldVersions.length} times
155+
</span>
156+
</>}
148157
</span>
149158
{item.subName &&
150159
<SubPopover sub={item.subName}>
@@ -171,8 +180,8 @@ export default function ItemInfo ({
171180
showActionDropdown &&
172181
<>
173182
<EditInfo
174-
item={item} edit={edit} canEdit={canEdit}
175-
setCanEdit={setCanEdit} toggleEdit={toggleEdit} editText={editText} editThreshold={editThreshold}
183+
item={item} edit={edit} canShadowEdit={canShadowEdit}
184+
setCanShadowEdit={setCanShadowEdit} toggleShadowEdit={toggleShadowEdit} shadowEditText={shadowEditText} shadowEditThreshold={shadowEditThreshold}
176185
/>
177186
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
178187
<ActionDropdown>
@@ -219,6 +228,13 @@ export default function ItemInfo ({
219228
<hr className='dropdown-divider' />
220229
<PinSubDropdownItem item={item} />
221230
</>}
231+
{item.mine && !item.position && !item.deletedAt && !item.bio && // TODO: adjust every edit
232+
<>
233+
<hr className='dropdown-divider' />
234+
<Link href={`/items/${item.id}/edit`} className='text-reset dropdown-item'>
235+
edit
236+
</Link>
237+
</>}
222238
{item.mine && !item.position && !item.deletedAt && !item.bio &&
223239
<>
224240
<hr className='dropdown-divider' />
@@ -339,37 +355,37 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) {
339355
)
340356
}
341357

342-
function EditInfo ({ item, edit, canEdit, setCanEdit, toggleEdit, editText, editThreshold }) {
358+
function EditInfo ({ item, edit, canShadowEdit, setCanShadowEdit, toggleShadowEdit, shadowEditText, shadowEditThreshold }) {
343359
const router = useRouter()
344360

345-
if (canEdit) {
361+
if (canShadowEdit) {
346362
return (
347363
<>
348364
<span> \ </span>
349365
<span
350366
className='text-reset pointer fw-bold font-monospace'
351-
onClick={() => toggleEdit ? toggleEdit() : router.push(`/items/${item.id}/edit`)}
367+
onClick={() => toggleShadowEdit ? toggleShadowEdit() : router.push(`/items/${item.id}/edit`)}
352368
>
353-
<span>{editText || 'edit'} </span>
369+
<span>{shadowEditText || 'edit'} </span>
354370
{(!item.invoice?.actionState || item.invoice?.actionState === 'PAID')
355371
? <Countdown
356-
date={editThreshold}
357-
onComplete={() => { setCanEdit(false) }}
372+
date={shadowEditThreshold}
373+
onComplete={() => { setCanShadowEdit(false) }}
358374
/>
359375
: <span>10:00</span>}
360376
</span>
361377
</>
362378
)
363379
}
364380

365-
if (edit && !canEdit) {
381+
if (edit && !canShadowEdit) {
366382
// if we're still editing after timer ran out
367383
return (
368384
<>
369385
<span> \ </span>
370386
<span
371387
className='text-reset pointer fw-bold font-monospace'
372-
onClick={() => toggleEdit ? toggleEdit() : router.push(`/items/${item.id}`)}
388+
onClick={() => toggleShadowEdit ? toggleShadowEdit() : router.push(`/items/${item.id}`)}
373389
>
374390
<span>cancel </span>
375391
<span>00:00</span>

components/job-form.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default function JobForm ({ item, sub }) {
134134

135135
export function JobButtonBar ({
136136
itemId, disable, className, children, handleStop, onCancel, hasCancel = true,
137-
createText = 'post', editText = 'save', stopText = 'remove'
137+
createText = 'post', shadowEditText = 'save', stopText = 'remove'
138138
}) {
139139
return (
140140
<div className={`mt-3 ${className}`}>
@@ -145,7 +145,7 @@ export function JobButtonBar ({
145145
<div className='d-flex align-items-center ms-auto'>
146146
{hasCancel && <CancelButton onClick={onCancel} />}
147147
<FeeButton
148-
text={itemId ? editText : createText}
148+
text={itemId ? shadowEditText : createText}
149149
variant='secondary'
150150
disabled={disable}
151151
/>

components/post.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export default function Post ({ sub }) {
187187
export function ItemButtonBar ({
188188
itemId, canDelete = true, disable,
189189
className, children, onDelete, onCancel, hasCancel = true,
190-
createText = 'post', editText = 'save', deleteText = 'delete'
190+
createText = 'post', shadowEditText = 'save', deleteText = 'delete'
191191
}) {
192192
const router = useRouter()
193193

@@ -205,7 +205,7 @@ export function ItemButtonBar ({
205205
<div className='d-flex align-items-center ms-auto'>
206206
{hasCancel && <CancelButton onClick={onCancel} />}
207207
<FeeButton
208-
text={itemId ? editText : createText}
208+
text={itemId ? shadowEditText : createText}
209209
variant='secondary'
210210
disabled={disable}
211211
/>

components/use-can-edit.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ import { datePivot } from '@/lib/time'
33
import { useMe } from '@/components/me'
44
import { ITEM_EDIT_SECONDS, USER_ID } from '@/lib/constants'
55

6-
export default function useCanEdit (item) {
6+
export default function useCanShadowEdit (item) {
77
const editThreshold = datePivot(new Date(item.invoice?.confirmedAt ?? item.createdAt), { seconds: ITEM_EDIT_SECONDS })
88
const { me } = useMe()
99

10-
// deleted items can never be edited and every item has a 10 minute edit window
10+
// deleted items can never be edited and every item has a 10 minute shadow edit window
1111
// except bios, they can always be edited but they should never show the countdown
12-
const noEdit = !!item.deletedAt || (Date.now() >= editThreshold) || item.bio
12+
const noEdit = !!item.deletedAt || item.bio // TODO: check for backwards compatibility
1313
const authorEdit = me && item.mine
14-
const [canEdit, setCanEdit] = useState(!noEdit && authorEdit)
14+
const [canShadowEdit, setCanShadowEdit] = useState(!noEdit && authorEdit)
1515

1616
useEffect(() => {
17-
// allow anon edits if they have the correct hmac for the item invoice
17+
// allow anon shadow edits if they have the correct hmac for the item invoice
1818
// (the server will verify the hmac)
1919
const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
2020
const anonEdit = !!invParams && !me && Number(item.user.id) === USER_ID.anon
21-
// anonEdit should not override canEdit, but only allow edits if they aren't already allowed
22-
setCanEdit(canEdit => canEdit || anonEdit)
21+
// anonEdit should not override canShadowEdit, but only allow edits if they aren't already allowed
22+
setCanShadowEdit(canShadowEdit => canShadowEdit || anonEdit)
2323
}, [])
2424

25-
return [canEdit, setCanEdit, editThreshold]
25+
return [canShadowEdit, setCanShadowEdit, editThreshold]
2626
}

components/wallet-buttonbar.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isConfigured } from '@/wallets/common'
66
export default function WalletButtonBar ({
77
wallet, disable,
88
className, children, onDelete, onCancel, hasCancel = true,
9-
createText = 'attach', deleteText = 'detach', editText = 'save'
9+
createText = 'attach', deleteText = 'detach', shadowEditText = 'save'
1010
}) {
1111
return (
1212
<div className={`mt-3 ${className}`}>
@@ -16,7 +16,7 @@ export default function WalletButtonBar ({
1616
{children}
1717
<div className='d-flex align-items-center ms-auto'>
1818
{hasCancel && <CancelButton onClick={onCancel} />}
19-
<SubmitButton variant='primary' disabled={disable}>{isConfigured(wallet) ? editText : createText}</SubmitButton>
19+
<SubmitButton variant='primary' disabled={disable}>{isConfigured(wallet) ? shadowEditText : createText}</SubmitButton>
2020
</div>
2121
</div>
2222
</div>

fragments/comments.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ export const COMMENT_FIELDS = gql`
4848
ncomments
4949
nDirectComments
5050
imgproxyUrls
51+
cloneBornAt
52+
cloneDiedAt
53+
oldVersions {
54+
id
55+
title
56+
text
57+
url
58+
createdAt
59+
updatedAt
60+
cloneBornAt
61+
cloneDiedAt
62+
}
5163
rel
5264
apiKey
5365
invoice {

fragments/items.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ export const ITEM_FIELDS = gql`
7373
uploadId
7474
mine
7575
imgproxyUrls
76+
cloneBornAt
77+
cloneDiedAt
78+
oldVersions {
79+
id
80+
title
81+
text
82+
url
83+
createdAt
84+
updatedAt
85+
cloneBornAt
86+
cloneDiedAt
87+
}
7688
rel
7789
apiKey
7890
invoice {

0 commit comments

Comments
 (0)