Skip to content

Image upload/resize#3114

Merged
feruzm merged 3 commits intodevelopmentfrom
img
Feb 18, 2026
Merged

Image upload/resize#3114
feruzm merged 3 commits intodevelopmentfrom
img

Conversation

@feruzm
Copy link
Copy Markdown
Member

@feruzm feruzm commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Added 30MB image upload size limit with user alerts for oversized files.
    • Applied image compression to camera and gallery uploads.
  • Bug Fixes

    • Prevented concurrent upvote/downvote operations.
    • Fixed UI state consistency during post and reply submission.
    • Improved posting spinner display during authority prompts.
    • Enhanced draft save response handling for multiple response formats.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

These changes enhance image upload handling with size validation and compression, add concurrency control to voting actions using a ref guard, and improve draft submission UX with immediate state feedback and spinner management across submission flows.

Changes

Cohort / File(s) Summary
Image Upload Validation
src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx
Introduces MAX_IMAGE_UPLOAD_SIZE constant (30MB) and IMAGE_COMPRESS_OPTIONS for image handling. Applies compression to image picker and camera options. Adds pre-upload size filtering with user alerts for oversized images before upload initiation.
Voting Concurrency Control
src/components/upvotePopover/container/upvotePopover.tsx
Adds isVotingRef guard to serialize voting actions and prevent concurrent upvote/downvote operations. Both voting functions check ref.current and return early if voting is in progress. Resets flag in finally blocks after async completion.
Draft & Editor Submission UX
src/screens/editor/container/editorContainer.tsx
Broadens addDraft response handling to support multiple response shapes. Introduces immediate UI state changes (isPostSending flag) at submission start to prevent race conditions and show spinner. Enhances posting authority prompt logic and ensures consistent state updates during draft saves and reply submissions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With files now safe from oversized fate,
And votes that won't race and conflate,
Drafts spin with grace, no timing to waste,
Our uploads compressed, submission paced!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Image upload/resize' is only partially related to the changeset. While image upload size handling is addressed, the changes also include unrelated features like voting concurrency prevention and draft save flow improvements. Revise the title to reflect all significant changes, such as 'Add image upload size constraints, voting concurrency guard, and draft save improvements' or focus the PR on a single concern.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch img

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/upvotePopover/container/upvotePopover.tsx (2)

212-227: ⚠️ Potential issue | 🟡 Minor

Pre-existing: invalid response early-return doesn't reset parent voting state.

When vote() succeeds but returns an invalid/empty response (lines 212–227), the code shows a toast and returns without calling _onVotingStart(0). The .finally() will correctly unlock isVotingRef, but the parent component's voting UI (controlled via _onVotingStart) may be left in a stale "voting in progress" state. This predates this PR, but since you're already touching this function, it's worth addressing.

Proposed fix
           if (!response || !response.id) {
             dispatch(
               toastNotification(
                 intl.formatMessage(
                   { id: 'alert.something_wrong_msg' },
                   {
                     message: intl.formatMessage({
                       id: 'alert.invalid_response',
                     }),
                   },
                 ),
               ),
             );
+            _onVotingStart ? _onVotingStart(0) : null;
             return;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/upvotePopover/container/upvotePopover.tsx` around lines 212 -
227, In the vote() function in upvotePopover.tsx, when the response is missing
or response.id is falsy, call _onVotingStart(0) to reset the parent voting UI
before showing the toast and returning; ensure this reset happens immediately in
that invalid-response branch so the parent isn't left in a "voting in progress"
state (the existing .finally() still clears isVotingRef, but it doesn't reset
the parent's UI).

183-289: ⚠️ Potential issue | 🟡 Minor

Guard could remain permanently locked if synchronous code throws before vote() is called.

isVotingRef.current = true is set at line 189, but the .finally() that resets it is chained on the vote() promise at line 203. If any of the synchronous statements between lines 190–201 throw (e.g., _updateVoteCache, _setUpvotePercent), the promise chain never starts and the guard is stuck at true, permanently blocking all future votes until the component remounts.

Wrapping the entire body in a try/finally would be more robust:

Proposed fix
   const _upvoteContent = async () => {
     if (isVotingRef.current) {
       return;
     }
 
     if (!isDownVoted) {
       isVotingRef.current = true;
-      const _onVotingStart = onVotingStartRef.current;
-      _closePopover();
-      _onVotingStart ? _onVotingStart(sliderValue) : null;
-
-      _setUpvotePercent(sliderValue);
-
-      const weight = sliderValue ? Math.trunc(sliderValue * 100) * 100 : 0;
-      const _author = content?.author;
-      const _permlink = content?.permlink;
-
-      console.log(`casting up vote: ${weight}`);
-      _updateVoteCache(_author, _permlink, amount, false, CacheStatus.PENDING);
-
-      vote(currentAccount, pinCode, _author, _permlink, weight)
-        .then((response) => {
+      try {
+        const _onVotingStart = onVotingStartRef.current;
+        _closePopover();
+        _onVotingStart ? _onVotingStart(sliderValue) : null;
+
+        _setUpvotePercent(sliderValue);
+
+        const weight = sliderValue ? Math.trunc(sliderValue * 100) * 100 : 0;
+        const _author = content?.author;
+        const _permlink = content?.permlink;
+
+        console.log(`casting up vote: ${weight}`);
+        _updateVoteCache(_author, _permlink, amount, false, CacheStatus.PENDING);
+
+        const response = await vote(currentAccount, pinCode, _author, _permlink, weight);
           // ... success handling ...
-        })
-        .catch((err) => {
+      } catch (err) {
           // ... error handling ...
-        })
-        .finally(() => {
-          isVotingRef.current = false;
-        });
+      } finally {
+        isVotingRef.current = false;
+      }
     } else {
       setIsDownVoted(false);
     }
   };

The same pattern applies to _downvoteContent. Since the function is already async, switching to await + try/catch/finally also simplifies readability.

In practice the risk is low (the synchronous calls are unlikely to throw), but since this is a guard that permanently locks out voting, the defensive pattern is warranted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/upvotePopover/container/upvotePopover.tsx` around lines 183 -
289, The guard is set by isVotingRef.current = true inside _upvoteContent but
can remain true if any synchronous code before vote() throws; wrap the voting
flow in a try/finally so the ref is always reset: move isVotingRef.current =
true to the top of _upvoteContent, then use try { perform _closePopover,
_onVotingStart, _setUpvotePercent, _updateVoteCache, await vote(...) and handle
response } catch (err) { handle errors as current .catch logic } finally {
isVotingRef.current = false }, and apply the same async/await +
try/catch/finally pattern to _downvoteContent to ensure the guard is always
cleared even on synchronous exceptions.
src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx (1)

271-280: ⚠️ Potential issue | 🟠 Major

element.sourceURL can be null on iOS — use fallback to element.path for HEIC conversion.

The codebase already uses sourceURL || path fallback in multiple locations (speakUploaderModal.tsx, speak.ts), indicating that sourceURL can be null or undefined. The HEIC conversion code should follow the same pattern.

If sourceURL is null, RNHeicConverter.convert({ path: null }) will fail. While the guard at line 274 (if (res && res.path)) prevents a crash, it silently leaves the element as HEIC mime-type and uploads it unconverted — the server is unlikely to accept raw HEIC.

Update the conversion to use a fallback:

🛡️ Proposed fix
-            const res = await RNHeicConverter.convert({ path: element.sourceURL });
+            const heicPath = element.sourceURL || element.path;
+            const res = await RNHeicConverter.convert({ path: heicPath });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx` around
lines 271 - 280, The HEIC conversion currently calls RNHeicConverter.convert
with element.sourceURL which can be null on iOS; change the call in the
uploadsGalleryModal HEIC branch to pass a fallback path (use element.sourceURL
|| element.path) so convert never receives null, then proceed to update
element.mime, element.path and element.filename and write back into media[i] as
before; locate the code block handling image/heic conversion (variables:
element, media, i, RNHeicConverter.convert) and replace the convert argument to
use the fallback path.
🧹 Nitpick comments (4)
src/components/upvotePopover/container/upvotePopover.tsx (1)

291-358: _onVotingStart reference is captured outside the guard — minor nit.

Line 296 captures onVotingStartRef.current before the if (isDownVoted) check at line 297. This is harmless but slightly inconsistent with _upvoteContent where the capture happens inside the guarded block (line 190). Consider moving line 296 inside the if (isDownVoted) block for symmetry and to avoid the unnecessary read when the else branch is taken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/upvotePopover/container/upvotePopover.tsx` around lines 291 -
358, Move the capture of onVotingStartRef.current into the guarded branch to
mirror _upvoteContent: inside the _downvoteContent function, relocate the line
const _onVotingStart = onVotingStartRef.current so it is declared within the if
(isDownVoted) block (near the start of that block) rather than before the if;
this avoids an unnecessary ref read when the else branch executes and keeps
behavior symmetric between _downvoteContent, _upvoteContent, isDownVoted and
onVotingStartRef.
src/screens/editor/container/editorContainer.tsx (2)

1073-1075: setState({ isPostSending: true }) at line 1075 makes the duplicate call at lines 1115-1117 dead code.

Now that isPostSending: true is set unconditionally early (line 1075) and only reset to false on the authority-prompt path (line 1089), the existing setState({ isPostSending: true }) at lines 1115-1117 is unreachable in a state where isPostSending is false. Consider removing it to keep the intent clear.

♻️ Proposed cleanup
       }

-      this.setState({
-        isPostSending: true,
-      });
-
       const { post } = this.state;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screens/editor/container/editorContainer.tsx` around lines 1073 - 1075,
The early unconditional setting of the submission flags (this._isSubmitting =
true and this.setState({ isPostSending: true }) in the submit flow) makes the
later duplicate setState call that sets isPostSending to true (the setState at
lines 1115-1117) dead code; remove that redundant setState to avoid confusion
and clarify intent. Update the submit handler (the method that toggles
this._isSubmitting and this.setState({ isPostSending })) by deleting the second
setState({ isPostSending: true }) call and keep only the initial flag set,
ensuring any path that resets isPostSending to false (the authority-prompt
reset) remains unchanged.

673-676: Add an Array.isArray guard before indexing response?.[0].

Without a guard, if addDraft ever returns a non-array value with a truthy [0] property (or, pathologically, a bare string), _resDraft will be set to that value, bypass the if (!_resDraft) check, and then silently produce undefined for _resDraft._id — corrupting draftId state with no thrown error to surface the bug.

♻️ Proposed fix
-          const _resDraft =
-            response?.drafts?.[0] || // array wrapper format
-            response?.[0] || // direct array format
-            (response?._id ? response : null); // single object format
+          const _resDraft =
+            response?.drafts?.[0] || // array wrapper format
+            (Array.isArray(response) ? response[0] : null) || // direct array format
+            (response?._id ? response : null); // single object format
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screens/editor/container/editorContainer.tsx` around lines 673 - 676, The
current fallback logic that sets _resDraft can index response via response?.[0]
without ensuring response is an array; update the expression used to compute
_resDraft (the variable assigned from response?.drafts?.[0] || response?.[0] ||
(response?._id ? response : null)) to only use the second branch when response
is an array (e.g., replace response?.[0] with a guarded check such as
Array.isArray(response) && response[0]); ensure the overall order still prefers
response.drafts[0], then response[0] (only if response is an array), then the
single-object fallback (response._id) so _resDraft and subsequent draftId logic
cannot become undefined silently.
src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx (1)

256-280: Size validation doesn't cover the HEIC → JPEG expansion path.

The item.size check at line 256 uses the picker-reported size before HEIC conversion. When RNHeicConverter.convert converts from the original sourceURL asset, the resulting JPEG can be larger than the picker-compressed value that passed the pre-check, meaning a HEIC file that passes the 30 MB guard could produce an oversized JPEG. The existing 413 server-error handler (lines 352–357) will ultimately catch this, but a post-conversion size re-check would give a cleaner client-side signal without an unnecessary upload attempt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx` around
lines 256 - 280, After converting HEIC to JPEG in the loop that uses
RNHeicConverter.convert, re-check the resulting file size against
MAX_IMAGE_UPLOAD_SIZE (update element.size from the conversion result or stat
the new path), and if it exceeds the limit remove it from media, show the same
Alert.alert(intl.formatMessage({ id: 'alert.fail' }), intl.formatMessage({ id:
'alert.payloadTooLarge' })), and abort early if media becomes empty; ensure this
logic is applied inside the loop that processes element.mime === 'image/heic' so
oversized JPEGs are caught client-side before upload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx`:
- Line 255: The inline comment near the image size guard is stale (mentions 8MB)
while the code uses MAX_IMAGE_UPLOAD_SIZE = 30000000; update the comment in
uploadsGalleryModal (around the image-filtering logic) to accurately reflect the
30 MB limit or reference the MAX_IMAGE_UPLOAD_SIZE constant (e.g., "filter out
images larger than MAX_IMAGE_UPLOAD_SIZE (30 MB)") so the comment matches the
actual guard.

---

Outside diff comments:
In `@src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx`:
- Around line 271-280: The HEIC conversion currently calls
RNHeicConverter.convert with element.sourceURL which can be null on iOS; change
the call in the uploadsGalleryModal HEIC branch to pass a fallback path (use
element.sourceURL || element.path) so convert never receives null, then proceed
to update element.mime, element.path and element.filename and write back into
media[i] as before; locate the code block handling image/heic conversion
(variables: element, media, i, RNHeicConverter.convert) and replace the convert
argument to use the fallback path.

In `@src/components/upvotePopover/container/upvotePopover.tsx`:
- Around line 212-227: In the vote() function in upvotePopover.tsx, when the
response is missing or response.id is falsy, call _onVotingStart(0) to reset the
parent voting UI before showing the toast and returning; ensure this reset
happens immediately in that invalid-response branch so the parent isn't left in
a "voting in progress" state (the existing .finally() still clears isVotingRef,
but it doesn't reset the parent's UI).
- Around line 183-289: The guard is set by isVotingRef.current = true inside
_upvoteContent but can remain true if any synchronous code before vote() throws;
wrap the voting flow in a try/finally so the ref is always reset: move
isVotingRef.current = true to the top of _upvoteContent, then use try { perform
_closePopover, _onVotingStart, _setUpvotePercent, _updateVoteCache, await
vote(...) and handle response } catch (err) { handle errors as current .catch
logic } finally { isVotingRef.current = false }, and apply the same async/await
+ try/catch/finally pattern to _downvoteContent to ensure the guard is always
cleared even on synchronous exceptions.

---

Nitpick comments:
In `@src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx`:
- Around line 256-280: After converting HEIC to JPEG in the loop that uses
RNHeicConverter.convert, re-check the resulting file size against
MAX_IMAGE_UPLOAD_SIZE (update element.size from the conversion result or stat
the new path), and if it exceeds the limit remove it from media, show the same
Alert.alert(intl.formatMessage({ id: 'alert.fail' }), intl.formatMessage({ id:
'alert.payloadTooLarge' })), and abort early if media becomes empty; ensure this
logic is applied inside the loop that processes element.mime === 'image/heic' so
oversized JPEGs are caught client-side before upload.

In `@src/components/upvotePopover/container/upvotePopover.tsx`:
- Around line 291-358: Move the capture of onVotingStartRef.current into the
guarded branch to mirror _upvoteContent: inside the _downvoteContent function,
relocate the line const _onVotingStart = onVotingStartRef.current so it is
declared within the if (isDownVoted) block (near the start of that block) rather
than before the if; this avoids an unnecessary ref read when the else branch
executes and keeps behavior symmetric between _downvoteContent, _upvoteContent,
isDownVoted and onVotingStartRef.

In `@src/screens/editor/container/editorContainer.tsx`:
- Around line 1073-1075: The early unconditional setting of the submission flags
(this._isSubmitting = true and this.setState({ isPostSending: true }) in the
submit flow) makes the later duplicate setState call that sets isPostSending to
true (the setState at lines 1115-1117) dead code; remove that redundant setState
to avoid confusion and clarify intent. Update the submit handler (the method
that toggles this._isSubmitting and this.setState({ isPostSending })) by
deleting the second setState({ isPostSending: true }) call and keep only the
initial flag set, ensuring any path that resets isPostSending to false (the
authority-prompt reset) remains unchanged.
- Around line 673-676: The current fallback logic that sets _resDraft can index
response via response?.[0] without ensuring response is an array; update the
expression used to compute _resDraft (the variable assigned from
response?.drafts?.[0] || response?.[0] || (response?._id ? response : null)) to
only use the second branch when response is an array (e.g., replace
response?.[0] with a guarded check such as Array.isArray(response) &&
response[0]); ensure the overall order still prefers response.drafts[0], then
response[0] (only if response is an array), then the single-object fallback
(response._id) so _resDraft and subsequent draftId logic cannot become undefined
silently.

Comment thread src/components/uploadsGalleryModal/container/uploadsGalleryModal.tsx Outdated
@feruzm feruzm merged commit 97308b0 into development Feb 18, 2026
0 of 2 checks passed
@feruzm feruzm deleted the img branch February 18, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant