Use Github style upload
* removed upfront fees * removed images provider since we no longer need to keep track of unsubmitted images on the client * removed User.images resolver * removed deleteImage mutation * use Github style upload where it shows ![Uploading <filename>...]() first and then replaces that with  after successful upload
This commit is contained in:
parent
167a4189ca
commit
30aed4e805
@ -19,7 +19,6 @@ import { sendUserNotification } from '../webPush'
|
|||||||
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } 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'
|
||||||
import { deleteObject } from '../s3'
|
|
||||||
|
|
||||||
export async function commentFilterClause (me, models) {
|
export async function commentFilterClause (me, models) {
|
||||||
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
||||||
@ -804,23 +803,6 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
|
||||||
deleteImage: async (parent, { id }, { me, models }) => {
|
|
||||||
if (!me) {
|
|
||||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const img = await models.upload.findUnique({ where: { id: Number(id) } })
|
|
||||||
if (img.userId !== me.id) {
|
|
||||||
throw new GraphQLError('not your image', { extensions: { code: 'FORBIDDEN' } })
|
|
||||||
}
|
|
||||||
if (img.itemId) {
|
|
||||||
throw new GraphQLError('image already included in an item', { extensions: { code: 'BAD_INPUT' } })
|
|
||||||
}
|
|
||||||
await models.upload.delete({ where: { id: Number(id) } })
|
|
||||||
await deleteObject(id)
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Item: {
|
Item: {
|
||||||
|
@ -1,35 +1,6 @@
|
|||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_TYPES_ALLOW } from '../../lib/constants'
|
import { IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_TYPES_ALLOW } from '../../lib/constants'
|
||||||
import { createPresignedPost } from '../s3'
|
import { createPresignedPost } from '../s3'
|
||||||
import { datePivot } from '../../lib/time'
|
|
||||||
import serialize from './serial'
|
|
||||||
|
|
||||||
// factor for bytes to megabyte
|
|
||||||
const MB = 1024 * 1024
|
|
||||||
// factor for msats to sats
|
|
||||||
const SATS = 1000
|
|
||||||
|
|
||||||
async function uploadCosts (models, userId, photoId, size) {
|
|
||||||
let { _sum: { size: sumSize } } = await models.upload.aggregate({
|
|
||||||
_sum: { size: true },
|
|
||||||
where: { userId, createdAt: { gt: datePivot(new Date(), { days: -1 }) }, id: photoId ? { not: photoId } : undefined }
|
|
||||||
})
|
|
||||||
// assume the image was already uploaded in the calculation
|
|
||||||
sumSize += size
|
|
||||||
if (sumSize <= 5 * MB) {
|
|
||||||
return 0 * SATS
|
|
||||||
}
|
|
||||||
if (sumSize <= 10 * MB) {
|
|
||||||
return 10 * SATS
|
|
||||||
}
|
|
||||||
if (sumSize <= 25 * MB) {
|
|
||||||
return 100 * SATS
|
|
||||||
}
|
|
||||||
if (sumSize <= 100 * MB) {
|
|
||||||
return 1000 * SATS
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -52,12 +23,6 @@ export default {
|
|||||||
|
|
||||||
const { photoId } = await models.user.findUnique({ where: { id: me.id } })
|
const { photoId } = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
const costs = avatar ? 0 : await uploadCosts(models, me.id, photoId, size)
|
|
||||||
if (costs < 0) {
|
|
||||||
throw new GraphQLError('image quota of 100 MB exceeded', { extensions: { code: 'BAD_INPUT' } })
|
|
||||||
}
|
|
||||||
const feeTx = models.user.update({ data: { msats: { decrement: costs } }, where: { id: me.id } })
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
type,
|
type,
|
||||||
size,
|
size,
|
||||||
@ -70,10 +35,10 @@ export default {
|
|||||||
if (avatar && photoId) uploadId = photoId
|
if (avatar && photoId) uploadId = photoId
|
||||||
if (uploadId) {
|
if (uploadId) {
|
||||||
// update upload record
|
// update upload record
|
||||||
await serialize(models, models.upload.update({ data, where: { id: uploadId } }), feeTx)
|
await models.upload.update({ data, where: { id: uploadId } })
|
||||||
} else {
|
} else {
|
||||||
// create upload record
|
// create upload record
|
||||||
const [upload] = await serialize(models, models.upload.create({ data }), feeTx)
|
const upload = await models.upload.create({ data })
|
||||||
uploadId = upload.id
|
uploadId = upload.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -896,16 +896,6 @@ export default {
|
|||||||
return contributors.has(user.name)
|
return contributors.has(user.name)
|
||||||
}
|
}
|
||||||
return !user.hideIsContributor && contributors.has(user.name)
|
return !user.hideIsContributor && contributors.has(user.name)
|
||||||
},
|
|
||||||
images: async (user, { submitted }, { me, models }) => {
|
|
||||||
if (!me) return null
|
|
||||||
const uploads = await models.upload.findMany({
|
|
||||||
where: {
|
|
||||||
userId: me.id,
|
|
||||||
itemId: submitted !== undefined ? submitted ? { not: null } : null : undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return uploads
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,6 @@ export default gql`
|
|||||||
dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Boolean!
|
dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Boolean!
|
||||||
act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
|
act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
|
||||||
pollVote(id: ID!, hash: String, hmac: String): ID!
|
pollVote(id: ID!, hash: String, hmac: String): ID!
|
||||||
deleteImage(id: ID!): ID!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PollOption {
|
type PollOption {
|
||||||
|
@ -113,6 +113,5 @@ export default gql`
|
|||||||
meSubscriptionPosts: Boolean!
|
meSubscriptionPosts: Boolean!
|
||||||
meSubscriptionComments: Boolean!
|
meSubscriptionComments: Boolean!
|
||||||
meMute: Boolean
|
meMute: Boolean
|
||||||
images(submitted: Boolean): [Image]
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -12,7 +12,6 @@ import { useCallback } from 'react'
|
|||||||
import { normalizeForwards } from '../lib/form'
|
import { normalizeForwards } from '../lib/form'
|
||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export function BountyForm ({
|
export function BountyForm ({
|
||||||
item,
|
item,
|
||||||
@ -28,7 +27,6 @@ export function BountyForm ({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
||||||
const [upsertBounty] = useMutation(
|
const [upsertBounty] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -57,11 +55,7 @@ export function BountyForm ({
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {
|
`
|
||||||
onCompleted ({ upsertBounty: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
@ -5,10 +5,8 @@ import { EditFeeButton } from './fee-button'
|
|||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import Delete from './delete'
|
import Delete from './delete'
|
||||||
import { commentSchema } from '../lib/validate'
|
import { commentSchema } from '../lib/validate'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
const [upsertComment] = useMutation(
|
const [upsertComment] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertComment($id: ID! $text: String!) {
|
mutation upsertComment($id: ID! $text: String!) {
|
||||||
@ -16,9 +14,6 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
onCompleted ({ upsertComment: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
},
|
|
||||||
update (cache, { data: { upsertComment } }) {
|
update (cache, { data: { upsertComment } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: `Item:${comment.id}`,
|
id: `Item:${comment.id}`,
|
||||||
|
@ -17,7 +17,6 @@ import { normalizeForwards } from '../lib/form'
|
|||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import useCrossposter from './use-crossposter'
|
import useCrossposter from './use-crossposter'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export function DiscussionForm ({
|
export function DiscussionForm ({
|
||||||
item, sub, editThreshold, titleLabel = 'title',
|
item, sub, editThreshold, titleLabel = 'title',
|
||||||
@ -31,7 +30,6 @@ export function DiscussionForm ({
|
|||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
const shareTitle = router.query.title
|
const shareTitle = router.query.title
|
||||||
const crossposter = useCrossposter()
|
const crossposter = useCrossposter()
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
|
|
||||||
const [upsertDiscussion] = useMutation(
|
const [upsertDiscussion] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -40,12 +38,7 @@ export function DiscussionForm ({
|
|||||||
id
|
id
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}`,
|
}`
|
||||||
{
|
|
||||||
onCompleted ({ upsertDiscussion: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
@ -25,8 +25,7 @@ import textAreaCaret from 'textarea-caret'
|
|||||||
import ReactDatePicker from 'react-datepicker'
|
import ReactDatePicker from 'react-datepicker'
|
||||||
import 'react-datepicker/dist/react-datepicker.css'
|
import 'react-datepicker/dist/react-datepicker.css'
|
||||||
import { debounce } from './use-debounce-callback'
|
import { debounce } from './use-debounce-callback'
|
||||||
import { ImageUpload, useImages } from './image'
|
import { ImageUpload } from './image'
|
||||||
import LinkIcon from '../svgs/link.svg'
|
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, value, onClick, disabled, cost, ...props
|
children, variant, value, onClick, disabled, cost, ...props
|
||||||
@ -95,23 +94,6 @@ export function InputSkeleton ({ label, hint }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageSelector ({ file, name, url, text, setText, onDelete }) {
|
|
||||||
const onLink = () => {
|
|
||||||
let newText = text ? text + '\n\n' : ''
|
|
||||||
newText += url
|
|
||||||
setText(newText)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className='d-flex align-items-center text-muted mt-2'>
|
|
||||||
<img className='me-1' src={file ? window.URL.createObjectURL(file) : url} width={32} height={32} style={{ objectFit: 'contain' }} />
|
|
||||||
<a href={url} target='_blank' rel='noreferrer'>{name || url}</a>
|
|
||||||
<LinkIcon className='ms-auto me-1' width={18} height={18} onClick={onLink} style={{ cursor: 'pointer' }} />
|
|
||||||
<CloseIcon width={18} height={18} onClick={onDelete} style={{ cursor: 'pointer' }} />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_MENTION_INDICES = { start: -1, end: -1 }
|
const DEFAULT_MENTION_INDICES = { start: -1, end: -1 }
|
||||||
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, onKeyDown, innerRef, ...props }) {
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, onKeyDown, innerRef, ...props }) {
|
||||||
const [tab, setTab] = useState('write')
|
const [tab, setTab] = useState('write')
|
||||||
@ -119,8 +101,6 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
||||||
innerRef = innerRef || useRef(null)
|
innerRef = innerRef || useRef(null)
|
||||||
const previousTab = useRef(tab)
|
const previousTab = useRef(tab)
|
||||||
const toaster = useToast()
|
|
||||||
const { unsubmittedImages, setUnsubmittedImages, deleteImage } = useImages()
|
|
||||||
|
|
||||||
props.as ||= TextareaAutosize
|
props.as ||= TextareaAutosize
|
||||||
props.rows ||= props.minRows || 6
|
props.rows ||= props.minRows || 6
|
||||||
@ -244,7 +224,17 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
<span className='ms-auto text-muted d-flex align-items-center'>
|
<span className='ms-auto text-muted d-flex align-items-center'>
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
className='d-flex align-items-center me-1'
|
className='d-flex align-items-center me-1'
|
||||||
onSuccess={img => setUnsubmittedImages(prev => [...prev, img])}
|
onSelect={file => {
|
||||||
|
let text = innerRef.current.value
|
||||||
|
if (text) text += '\n\n'
|
||||||
|
text += `![Uploading ${file.name}…]()`
|
||||||
|
helpers.setValue(text)
|
||||||
|
}}
|
||||||
|
onSuccess={({ url, name }) => {
|
||||||
|
let text = innerRef.current.value
|
||||||
|
text = text.replace(`![Uploading ${name}…]()`, ``)
|
||||||
|
helpers.setValue(text)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AddImageIcon width={18} height={18} />
|
<AddImageIcon width={18} height={18} />
|
||||||
</ImageUpload>
|
</ImageUpload>
|
||||||
@ -270,27 +260,6 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
onBlur={() => setTimeout(resetSuggestions, 100)}
|
onBlur={() => setTimeout(resetSuggestions, 100)}
|
||||||
/>)}
|
/>)}
|
||||||
</UserSuggest>
|
</UserSuggest>
|
||||||
{unsubmittedImages.map((img, i) => {
|
|
||||||
return (
|
|
||||||
<ImageSelector
|
|
||||||
name={img.name}
|
|
||||||
file={img.file}
|
|
||||||
url={img.url}
|
|
||||||
key={i}
|
|
||||||
text={meta.value}
|
|
||||||
setText={helpers.setValue}
|
|
||||||
onDelete={async () => {
|
|
||||||
try {
|
|
||||||
await deleteImage({ variables: { id: img.id } })
|
|
||||||
toaster.success('image deleted')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toaster.danger('failed to delete image')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{tab !== 'write' &&
|
{tab !== 'write' &&
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import styles from './text.module.css'
|
import styles from './text.module.css'
|
||||||
import { Fragment, useState, useEffect, useMemo, useCallback, useRef, createContext, useContext } from 'react'
|
import { Fragment, useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
||||||
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
@ -7,81 +7,7 @@ import { Dropdown } from 'react-bootstrap'
|
|||||||
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
|
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { useMutation, useQuery } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { extractUrls } from '../lib/md'
|
|
||||||
|
|
||||||
const ImageContext = createContext({ unsubmitted: [] })
|
|
||||||
|
|
||||||
const imageIdToUrl = id => `https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/${id}`
|
|
||||||
|
|
||||||
export function ImageProvider ({ me, children }) {
|
|
||||||
const { data, loading } = useQuery(
|
|
||||||
gql`
|
|
||||||
query images($submitted: Boolean) {
|
|
||||||
me {
|
|
||||||
images(submitted: $submitted) {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
type
|
|
||||||
size
|
|
||||||
width
|
|
||||||
height
|
|
||||||
itemId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, {
|
|
||||||
variables: { submitted: false }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const [deleteImage] = useMutation(gql`
|
|
||||||
mutation deleteImage($id: ID!) {
|
|
||||||
deleteImage(id: $id)
|
|
||||||
}`, {
|
|
||||||
onCompleted: (_, options) => {
|
|
||||||
const id = options.variables.id
|
|
||||||
setUnsubmittedImages(prev => prev.filter(img => img.id !== id))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [unsubmittedImages, setUnsubmittedImages] = useState([])
|
|
||||||
|
|
||||||
const markImagesAsSubmitted = useCallback((text) => {
|
|
||||||
// mark images from S3 included in the text as submitted on the client
|
|
||||||
const urls = extractUrls(text)
|
|
||||||
const s3UrlPrefix = `https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/`
|
|
||||||
urls
|
|
||||||
.filter(url => url.startsWith(s3UrlPrefix))
|
|
||||||
.forEach(url => {
|
|
||||||
const s3Key = url.split('/').pop()
|
|
||||||
setUnsubmittedImages(prev => prev.filter(img => img.id !== s3Key))
|
|
||||||
})
|
|
||||||
}, [setUnsubmittedImages])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const images = data?.me?.images
|
|
||||||
if (images) {
|
|
||||||
setUnsubmittedImages(images.map(img => ({ ...img, url: imageIdToUrl(img.id) })))
|
|
||||||
}
|
|
||||||
}, [setUnsubmittedImages, loading])
|
|
||||||
|
|
||||||
const contextValue = {
|
|
||||||
unsubmittedImages,
|
|
||||||
setUnsubmittedImages,
|
|
||||||
deleteImage,
|
|
||||||
markImagesAsSubmitted
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ImageContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</ImageContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useImages () {
|
|
||||||
const images = useContext(ImageContext)
|
|
||||||
return images
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeOriginalUrl (imgproxyUrl) {
|
export function decodeOriginalUrl (imgproxyUrl) {
|
||||||
const parts = imgproxyUrl.split('/')
|
const parts = imgproxyUrl.split('/')
|
||||||
@ -228,6 +154,7 @@ export function ImageUpload ({ children, className, onSelect, onSuccess }) {
|
|||||||
const img = new window.Image()
|
const img = new window.Image()
|
||||||
img.src = window.URL.createObjectURL(file)
|
img.src = window.URL.createObjectURL(file)
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
|
onSelect?.(file)
|
||||||
let data
|
let data
|
||||||
const variables = {
|
const variables = {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
|
@ -18,7 +18,6 @@ import ActionTooltip from './action-tooltip'
|
|||||||
import { jobSchema } from '../lib/validate'
|
import { jobSchema } from '../lib/validate'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
function satsMin2Mo (minute) {
|
function satsMin2Mo (minute) {
|
||||||
return minute * 30 * 24 * 60
|
return minute * 30 * 24 * 60
|
||||||
@ -41,7 +40,6 @@ export default function JobForm ({ item, sub }) {
|
|||||||
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [logoId, setLogoId] = useState(item?.uploadId)
|
const [logoId, setLogoId] = useState(item?.uploadId)
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
const [upsertJob] = useMutation(gql`
|
const [upsertJob] = useMutation(gql`
|
||||||
mutation upsertJob($sub: String!, $id: ID, $title: String!, $company: String!, $location: String,
|
mutation upsertJob($sub: String!, $id: ID, $title: String!, $company: String!, $location: String,
|
||||||
$remote: Boolean, $text: String!, $url: String!, $maxBid: Int!, $status: String, $logo: Int, $hash: String, $hmac: String) {
|
$remote: Boolean, $text: String!, $url: String!, $maxBid: Int!, $status: String, $logo: Int, $hash: String, $hmac: String) {
|
||||||
@ -51,11 +49,7 @@ export default function JobForm ({ item, sub }) {
|
|||||||
id
|
id
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}`, {
|
}`
|
||||||
onCompleted ({ upsertJob: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
@ -17,7 +17,6 @@ import CancelButton from './cancel-button'
|
|||||||
import { normalizeForwards } from '../lib/form'
|
import { normalizeForwards } from '../lib/form'
|
||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -53,7 +52,6 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
const { markImagesAsSubmitted } = useImages
|
|
||||||
|
|
||||||
const related = []
|
const related = []
|
||||||
for (const item of relatedData?.related?.items || []) {
|
for (const item of relatedData?.related?.items || []) {
|
||||||
@ -77,12 +75,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
id
|
id
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}`,
|
}`
|
||||||
{
|
|
||||||
onCompleted ({ upsertLink: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
@ -13,14 +13,12 @@ import CancelButton from './cancel-button'
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { normalizeForwards } from '../lib/form'
|
import { normalizeForwards } from '../lib/form'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
|
|
||||||
const [upsertPoll] = useMutation(
|
const [upsertPoll] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -31,11 +29,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||||||
id
|
id
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}`, {
|
}`
|
||||||
onCompleted ({ upsertPoll: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
@ -11,7 +11,6 @@ import { commentSchema } from '../lib/validate'
|
|||||||
import Info from './info'
|
import Info from './info'
|
||||||
import { quote } from '../lib/md'
|
import { quote } from '../lib/md'
|
||||||
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||||
import { useImages } from './image'
|
|
||||||
|
|
||||||
export function ReplyOnAnotherPage ({ item }) {
|
export function ReplyOnAnotherPage ({ item }) {
|
||||||
const path = item.path.split('.')
|
const path = item.path.split('.')
|
||||||
@ -50,7 +49,6 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
const formInnerRef = useRef()
|
const formInnerRef = useRef()
|
||||||
const { markImagesAsSubmitted } = useImages()
|
|
||||||
|
|
||||||
// Start block to handle iOS Safari's weird selection clearing behavior
|
// Start block to handle iOS Safari's weird selection clearing behavior
|
||||||
const savedRange = useRef()
|
const savedRange = useRef()
|
||||||
@ -118,9 +116,6 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
onCompleted ({ upsertComment: { text } }) {
|
|
||||||
markImagesAsSubmitted(text)
|
|
||||||
},
|
|
||||||
update (cache, { data: { upsertComment } }) {
|
update (cache, { data: { upsertComment } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: `Item:${parentId}`,
|
id: `Item:${parentId}`,
|
||||||
|
@ -17,7 +17,6 @@ import { SSR } from '../lib/constants'
|
|||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
import { LoggerProvider } from '../components/logger'
|
import { LoggerProvider } from '../components/logger'
|
||||||
import { ImageProvider } from '../components/image'
|
|
||||||
|
|
||||||
NProgress.configure({
|
NProgress.configure({
|
||||||
showSpinner: false
|
showSpinner: false
|
||||||
@ -90,23 +89,21 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||||||
<PlausibleProvider domain='stacker.news' trackOutboundLinks>
|
<PlausibleProvider domain='stacker.news' trackOutboundLinks>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<MeProvider me={me}>
|
<MeProvider me={me}>
|
||||||
<ImageProvider>
|
<LoggerProvider>
|
||||||
<LoggerProvider>
|
<ServiceWorkerProvider>
|
||||||
<ServiceWorkerProvider>
|
<PriceProvider price={price}>
|
||||||
<PriceProvider price={price}>
|
<LightningProvider>
|
||||||
<LightningProvider>
|
<ToastProvider>
|
||||||
<ToastProvider>
|
<ShowModalProvider>
|
||||||
<ShowModalProvider>
|
<BlockHeightProvider blockHeight={blockHeight}>
|
||||||
<BlockHeightProvider blockHeight={blockHeight}>
|
<Component ssrData={ssrData} {...otherProps} />
|
||||||
<Component ssrData={ssrData} {...otherProps} />
|
</BlockHeightProvider>
|
||||||
</BlockHeightProvider>
|
</ShowModalProvider>
|
||||||
</ShowModalProvider>
|
</ToastProvider>
|
||||||
</ToastProvider>
|
</LightningProvider>
|
||||||
</LightningProvider>
|
</PriceProvider>
|
||||||
</PriceProvider>
|
</ServiceWorkerProvider>
|
||||||
</ServiceWorkerProvider>
|
</LoggerProvider>
|
||||||
</LoggerProvider>
|
|
||||||
</ImageProvider>
|
|
||||||
</MeProvider>
|
</MeProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user