diff --git a/components/bounty-form.js b/components/bounty-form.js index f1b14bf46..740b23c09 100644 --- a/components/bounty-form.js +++ b/components/bounty-form.js @@ -12,6 +12,7 @@ import { useCallback } from 'react' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' import { useMe } from './me' +import { useImages } from './image' export function BountyForm ({ item, @@ -27,6 +28,7 @@ export function BountyForm ({ const router = useRouter() const client = useApolloClient() const me = useMe() + const { markImagesAsSubmitted } = useImages() const schema = bountySchema({ client, me, existingBoost: item?.boost }) const [upsertBounty] = useMutation( gql` @@ -55,7 +57,11 @@ export function BountyForm ({ id } } - ` + `, { + onCompleted ({ upsertBounty: { text } }) { + markImagesAsSubmitted(text) + } + } ) const onSubmit = useCallback( diff --git a/components/comment-edit.js b/components/comment-edit.js index 0f6e1077d..7dcd8f494 100644 --- a/components/comment-edit.js +++ b/components/comment-edit.js @@ -5,8 +5,10 @@ import { EditFeeButton } from './fee-button' import Button from 'react-bootstrap/Button' import Delete from './delete' import { commentSchema } from '../lib/validate' +import { useImages } from './image' export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) { + const { markImagesAsSubmitted } = useImages() const [upsertComment] = useMutation( gql` mutation upsertComment($id: ID! $text: String!) { @@ -14,6 +16,9 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc text } }`, { + onCompleted ({ upsertComment: { text } }) { + markImagesAsSubmitted(text) + }, update (cache, { data: { upsertComment } }) { cache.modify({ id: `Item:${comment.id}`, diff --git a/components/discussion-form.js b/components/discussion-form.js index caf06a7b8..9443796e3 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -17,6 +17,7 @@ import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' import { useMe } from './me' import useCrossposter from './use-crossposter' +import { useImages } from './image' export function DiscussionForm ({ item, sub, editThreshold, titleLabel = 'title', @@ -30,14 +31,21 @@ export function DiscussionForm ({ // if Web Share Target API was used const shareTitle = router.query.title const crossposter = useCrossposter() + const { markImagesAsSubmitted } = useImages() const [upsertDiscussion] = useMutation( gql` mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: [ItemForwardInput], $hash: String, $hmac: String) { upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) { id + text } - }` + }`, + { + onCompleted ({ upsertDiscussion: { text } }) { + markImagesAsSubmitted(text) + } + } ) const onSubmit = useCallback( diff --git a/components/image.js b/components/image.js index 6c99de160..337c8730d 100644 --- a/components/image.js +++ b/components/image.js @@ -8,6 +8,7 @@ import { UPLOAD_TYPES_ALLOW } from '../lib/constants' import { useToast } from './toast' import gql from 'graphql-tag' import { useMutation, useQuery } from '@apollo/client' +import { extractUrls } from '../lib/md' const ImageContext = createContext({ unsubmitted: [] }) @@ -44,6 +45,18 @@ export function ImageProvider ({ me, children }) { }) 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) { @@ -54,7 +67,8 @@ export function ImageProvider ({ me, children }) { const contextValue = { unsubmittedImages, setUnsubmittedImages, - deleteImage + deleteImage, + markImagesAsSubmitted } return ( diff --git a/components/job-form.js b/components/job-form.js index 2ea99b1e5..6f9b02dec 100644 --- a/components/job-form.js +++ b/components/job-form.js @@ -18,6 +18,7 @@ import ActionTooltip from './action-tooltip' import { jobSchema } from '../lib/validate' import CancelButton from './cancel-button' import { MAX_TITLE_LENGTH } from '../lib/constants' +import { useImages } from './image' function satsMin2Mo (minute) { return minute * 30 * 24 * 60 @@ -40,6 +41,7 @@ export default function JobForm ({ item, sub }) { const storageKeyPrefix = item ? undefined : `${sub.name}-job` const router = useRouter() const [logoId, setLogoId] = useState(item?.uploadId) + const { markImagesAsSubmitted } = useImages() const [upsertJob] = useMutation(gql` 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) { @@ -47,8 +49,13 @@ export default function JobForm ({ item, sub }) { location: $location, remote: $remote, text: $text, url: $url, maxBid: $maxBid, status: $status, logo: $logo, hash: $hash, hmac: $hmac) { id + text } - }` + }`, { + onCompleted ({ upsertJob: { text } }) { + markImagesAsSubmitted(text) + } + } ) const onSubmit = useCallback( diff --git a/components/link-form.js b/components/link-form.js index 8770bb507..98f808230 100644 --- a/components/link-form.js +++ b/components/link-form.js @@ -17,6 +17,7 @@ import CancelButton from './cancel-button' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' import { useMe } from './me' +import { useImages } from './image' export function LinkForm ({ item, sub, editThreshold, children }) { const router = useRouter() @@ -52,6 +53,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) { } } }`) + const { markImagesAsSubmitted } = useImages const related = [] for (const item of relatedData?.related?.items || []) { @@ -73,8 +75,14 @@ export function LinkForm ({ item, sub, editThreshold, children }) { mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: [ItemForwardInput], $hash: String, $hmac: String) { upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) { id + text } - }` + }`, + { + onCompleted ({ upsertLink: { text } }) { + markImagesAsSubmitted(text) + } + } ) const onSubmit = useCallback( diff --git a/components/poll-form.js b/components/poll-form.js index 6e6788782..257b6986d 100644 --- a/components/poll-form.js +++ b/components/poll-form.js @@ -13,12 +13,14 @@ import CancelButton from './cancel-button' import { useCallback } from 'react' import { normalizeForwards } from '../lib/form' import { useMe } from './me' +import { useImages } from './image' export function PollForm ({ item, sub, editThreshold, children }) { const router = useRouter() const client = useApolloClient() const me = useMe() const schema = pollSchema({ client, me, existingBoost: item?.boost }) + const { markImagesAsSubmitted } = useImages() const [upsertPoll] = useMutation( gql` @@ -27,8 +29,13 @@ export function PollForm ({ item, sub, editThreshold, children }) { upsertPoll(sub: $sub, id: $id, title: $title, text: $text, options: $options, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) { id + text } - }` + }`, { + onCompleted ({ upsertPoll: { text } }) { + markImagesAsSubmitted(text) + } + } ) const onSubmit = useCallback( diff --git a/components/reply.js b/components/reply.js index 8e41d9683..fceecb564 100644 --- a/components/reply.js +++ b/components/reply.js @@ -10,6 +10,7 @@ import { commentsViewedAfterComment } from '../lib/new-comments' import { commentSchema } from '../lib/validate' import Info from './info' import { quote } from '../lib/md' +import { useImages } from './image' export function ReplyOnAnotherPage ({ parentId }) { return ( @@ -40,6 +41,7 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children const parentId = item.id const replyInput = useRef(null) const formInnerRef = useRef() + const { markImagesAsSubmitted } = useImages() // Start block to handle iOS Safari's weird selection clearing behavior const savedRange = useRef() @@ -107,6 +109,9 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children } } }`, { + onCompleted ({ upsertComment: { text } }) { + markImagesAsSubmitted(text) + }, update (cache, { data: { upsertComment } }) { cache.modify({ id: `Item:${parentId}`,